@sigrun/sigrun 0.9.0 → 0.10.0
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/CHANGELOG.md +20 -0
- package/dist/jobs/index.js +3 -0
- package/dist/jobs/spec.js +15 -1
- package/dist/server/core-views/jobs-form.js +335 -0
- package/dist/server/core-views/jobs.js +300 -15
- package/dist/server/index.js +2 -1
- package/node_modules/@sigrun/shared/CHANGELOG.md +5 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +4 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +8 -8
- package/webui/dist/assets/{index-DGIrLC6F.js → index-By63QKOA.js} +1 -1
- package/webui/dist/index.html +1 -1
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
* principal has no view at all (`access: "none"` — the surface
|
|
31
31
|
* route's 404).
|
|
32
32
|
*/
|
|
33
|
+
import { JobDeclaredError, JobNotFoundError, } from "../../jobs/index.js";
|
|
33
34
|
import { jobAccess } from "../jobs-access.js";
|
|
34
35
|
import { isSuperuser } from "../auth/principal.js";
|
|
36
|
+
import { createSpecOf, emptyForm, formFromJob, formValuesOf, jobFormFields, savePatchOf, } from "./jobs-form.js";
|
|
35
37
|
/** The runs page size and the revisions page size (the plan refinement). */
|
|
36
38
|
export const PAGE_SIZE = 25;
|
|
37
39
|
/** A schedule's summary text (the JobsPanel rule, verbatim). */
|
|
@@ -45,6 +47,23 @@ function scheduleSummary(schedule) {
|
|
|
45
47
|
return `every ${schedule.every}`;
|
|
46
48
|
return `at ${schedule.at}`;
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The provenance line for one job, derived from its id (the id prefix IS
|
|
52
|
+
* the source). Exported for the view's tests (jobs-editor Task 2).
|
|
53
|
+
*/
|
|
54
|
+
export function definedInOf(id, source) {
|
|
55
|
+
if (source === "file") {
|
|
56
|
+
return `settings.json (jobs → ${id.slice("file:".length)})`;
|
|
57
|
+
}
|
|
58
|
+
if (source === "extension") {
|
|
59
|
+
return `the ${extensionSlugOf(id)} extension`;
|
|
60
|
+
}
|
|
61
|
+
return "created in sigrun";
|
|
62
|
+
}
|
|
63
|
+
/** The declaring extension's slug (`ext:<slug>:<name>` — the middle). */
|
|
64
|
+
function extensionSlugOf(id) {
|
|
65
|
+
return id.slice("ext:".length).split(":")[0];
|
|
66
|
+
}
|
|
48
67
|
/** One job's sidebar/header summary fields. */
|
|
49
68
|
function summaryFields(row) {
|
|
50
69
|
return {
|
|
@@ -83,6 +102,8 @@ function itemIdOf(payload) {
|
|
|
83
102
|
/** Parse the instance params (loose JSON → the typed shape; bad types drop). */
|
|
84
103
|
function paramsOf(params) {
|
|
85
104
|
const out = {};
|
|
105
|
+
if (params.edit === true)
|
|
106
|
+
out.edit = true;
|
|
86
107
|
if (typeof params.job === "string" && params.job !== "")
|
|
87
108
|
out.job = params.job;
|
|
88
109
|
if (Number.isInteger(params.revision) && params.revision > 0) {
|
|
@@ -132,7 +153,7 @@ function specJson(revision) {
|
|
|
132
153
|
* otherwise `found: false` (the main empty state), exactly the list's
|
|
133
154
|
* absence rule.
|
|
134
155
|
*/
|
|
135
|
-
async function readData(jobs, principal, params) {
|
|
156
|
+
async function readData(jobs, principal, params, models) {
|
|
136
157
|
const rows = [];
|
|
137
158
|
let selected;
|
|
138
159
|
for (const wire of await jobs.list()) {
|
|
@@ -140,7 +161,12 @@ async function readData(jobs, principal, params) {
|
|
|
140
161
|
const access = jobAccess(principal, row);
|
|
141
162
|
if (access === "none")
|
|
142
163
|
continue;
|
|
143
|
-
rows.push({
|
|
164
|
+
rows.push({
|
|
165
|
+
id: row.id,
|
|
166
|
+
name: row.name,
|
|
167
|
+
description: row.description ?? null,
|
|
168
|
+
...summaryFields(row),
|
|
169
|
+
});
|
|
144
170
|
if (params.job !== undefined && row.id === params.job) {
|
|
145
171
|
selected = { ...row, access };
|
|
146
172
|
}
|
|
@@ -152,7 +178,16 @@ async function readData(jobs, principal, params) {
|
|
|
152
178
|
revisions: { items: [], hasMore: false, nextRevCursor: null },
|
|
153
179
|
};
|
|
154
180
|
if (selected === undefined)
|
|
155
|
-
return { jobs: rows, main };
|
|
181
|
+
return { jobs: rows, main, form: emptyForm(), modelOptions: models() };
|
|
182
|
+
// The Task 4 edit seed: `{job, edit}` on a WRITABLE api job seeds
|
|
183
|
+
// `/form` from the job's current spec (a declared or read job in edit
|
|
184
|
+
// mode renders the details instead — the details keep the plain
|
|
185
|
+
// empty form, which the tree never shows there).
|
|
186
|
+
const form = params.edit === true &&
|
|
187
|
+
selected.source === "api" &&
|
|
188
|
+
selected.access === "write"
|
|
189
|
+
? formFromJob(selected)
|
|
190
|
+
: emptyForm();
|
|
156
191
|
// Runs: newest first; the cursor IS a run key (opaque to clients).
|
|
157
192
|
// The service's list is bounded by the retention cap, so the view
|
|
158
193
|
// fetches it whole and pages in memory.
|
|
@@ -178,9 +213,30 @@ async function readData(jobs, principal, params) {
|
|
|
178
213
|
nextRevCursor: nextRev < allRevisions.length ? nextRev : null,
|
|
179
214
|
};
|
|
180
215
|
main.found = true;
|
|
216
|
+
const action = selected.action;
|
|
181
217
|
main.job = {
|
|
182
218
|
id: selected.id,
|
|
183
219
|
name: selected.name,
|
|
220
|
+
description: selected.description ?? null,
|
|
221
|
+
definedIn: definedInOf(selected.id, selected.source),
|
|
222
|
+
owner: selected.owner ?? null,
|
|
223
|
+
source: selected.source,
|
|
224
|
+
kind: action.kind,
|
|
225
|
+
prompt: action.kind === "prompt" ? action.prompt : null,
|
|
226
|
+
model: action.kind === "prompt" ? (action.model ?? null) : null,
|
|
227
|
+
thinking: action.kind === "prompt" ? (action.thinking ?? null) : null,
|
|
228
|
+
conversation: action.kind === "prompt"
|
|
229
|
+
? action.conversation === "new"
|
|
230
|
+
? "new"
|
|
231
|
+
: `#${action.conversation}`
|
|
232
|
+
: null,
|
|
233
|
+
deliver: action.kind === "prompt" && action.deliver !== undefined
|
|
234
|
+
? action.deliver.join(", ")
|
|
235
|
+
: null,
|
|
236
|
+
task: action.kind === "task" ? action.task : null,
|
|
237
|
+
taskInputMarkdown: action.kind === "task" && action.input !== undefined
|
|
238
|
+
? `\`\`\`json\n${JSON.stringify(action.input, null, 2)}\n\`\`\``
|
|
239
|
+
: null,
|
|
184
240
|
...summaryFields(selected),
|
|
185
241
|
state: selected.state,
|
|
186
242
|
enabled: selected.enabled,
|
|
@@ -189,7 +245,7 @@ async function readData(jobs, principal, params) {
|
|
|
189
245
|
// the fallback only satisfies the narrowing.
|
|
190
246
|
access: selected.access === "none" ? "read" : selected.access,
|
|
191
247
|
};
|
|
192
|
-
return { jobs: rows, main };
|
|
248
|
+
return { jobs: rows, main, form, modelOptions: models() };
|
|
193
249
|
}
|
|
194
250
|
/** The one revision selected for the diff, plus its previous (baseline). */
|
|
195
251
|
function selectDiff(revisions, page, version) {
|
|
@@ -207,6 +263,12 @@ function selectDiff(revisions, page, version) {
|
|
|
207
263
|
/** The view surface (`SurfaceDef<"view">`), built over the jobs service. */
|
|
208
264
|
export function createJobsView(deps) {
|
|
209
265
|
const { jobs } = deps;
|
|
266
|
+
/** The model options: Default first (id "default"), then the
|
|
267
|
+
* catalog rows (id is the full `provider/modelId`). */
|
|
268
|
+
const modelOptions = () => [
|
|
269
|
+
{ id: "default", label: "Default" },
|
|
270
|
+
...deps.models(),
|
|
271
|
+
];
|
|
210
272
|
/** The caller's directory-aware principal (undefined user = anonymous).
|
|
211
273
|
* Whole-branch review I3: the USERLESS SUPERUSER (the naive
|
|
212
274
|
* single-operator loopback principal) is admitted — the /v1/jobs
|
|
@@ -246,8 +308,9 @@ export function createJobsView(deps) {
|
|
|
246
308
|
onActivate: { type: "edit", name: "openJob" },
|
|
247
309
|
item: {
|
|
248
310
|
title: { $: "./name" },
|
|
249
|
-
subtitle: { $: "./
|
|
311
|
+
subtitle: { $: "./description" },
|
|
250
312
|
meta: [
|
|
313
|
+
{ text: { $: "./scheduleSummary" }, icon: "clock" },
|
|
251
314
|
{ text: { $: "./nextRun" }, icon: "clock" },
|
|
252
315
|
{ text: { $: "./lastOutcome" }, icon: "info" },
|
|
253
316
|
],
|
|
@@ -285,7 +348,29 @@ export function createJobsView(deps) {
|
|
|
285
348
|
// Blocks are built as loose JSON objects (the validator owns the
|
|
286
349
|
// wire shape; the service validates every tree before serving).
|
|
287
350
|
const main = [];
|
|
288
|
-
|
|
351
|
+
// `{edit: true}` alone is the New-job form; `{job, edit}` edits THAT
|
|
352
|
+
// job. A declared (file:/ext:) or read-access job in edit mode falls
|
|
353
|
+
// through to the DETAILS (the editor is api+write only); an unknown
|
|
354
|
+
// `job` param with edit keeps the Task 3 rule (the form wins over
|
|
355
|
+
// "Job not found") only when NO job is selected.
|
|
356
|
+
const editingJob = params.edit === true &&
|
|
357
|
+
params.job !== undefined &&
|
|
358
|
+
data.main.found &&
|
|
359
|
+
data.main.job !== null &&
|
|
360
|
+
data.main.job.source === "api" &&
|
|
361
|
+
data.main.job.access === "write";
|
|
362
|
+
if (params.edit === true && (params.job === undefined || editingJob)) {
|
|
363
|
+
main.push({
|
|
364
|
+
type: "form",
|
|
365
|
+
id: "job-form",
|
|
366
|
+
bind: "/form",
|
|
367
|
+
mode: "submit",
|
|
368
|
+
onSubmit: editingJob ? "saveJob" : "createJob",
|
|
369
|
+
submitLabel: editingJob ? "Save" : "Create",
|
|
370
|
+
children: jobFormFields(editingJob && data.main.job !== null ? data.main.job.kind : "prompt"),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
else if (data.main.found && data.main.job !== null) {
|
|
289
374
|
const job = data.main.job;
|
|
290
375
|
// IMPORTANT 3 (review fix): per-run conversation buttons ride
|
|
291
376
|
// the card's `actions` as per-row LITERALS. The block contract's
|
|
@@ -326,6 +411,38 @@ export function createJobsView(deps) {
|
|
|
326
411
|
},
|
|
327
412
|
},
|
|
328
413
|
},
|
|
414
|
+
// Edit/Delete are api-only (declared jobs are read-only
|
|
415
|
+
// declarations — the `job-declared` callout says where to
|
|
416
|
+
// edit them). The confirm copy promises the run history
|
|
417
|
+
// survives (deleteApi retires the record; history routes
|
|
418
|
+
// answer for retired ids).
|
|
419
|
+
...(job.source === "api"
|
|
420
|
+
? [
|
|
421
|
+
{
|
|
422
|
+
id: "job-edit",
|
|
423
|
+
label: "Edit",
|
|
424
|
+
icon: "edit",
|
|
425
|
+
action: {
|
|
426
|
+
type: "navigate",
|
|
427
|
+
params: { job: job.id, edit: true },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "job-delete",
|
|
432
|
+
label: "Delete",
|
|
433
|
+
icon: "delete",
|
|
434
|
+
action: {
|
|
435
|
+
type: "submit",
|
|
436
|
+
name: "deleteJob",
|
|
437
|
+
confirm: {
|
|
438
|
+
title: "Delete this job?",
|
|
439
|
+
body: "Its run history is kept.",
|
|
440
|
+
tone: "bad",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
]
|
|
445
|
+
: []),
|
|
329
446
|
]
|
|
330
447
|
: [];
|
|
331
448
|
main.push({
|
|
@@ -336,14 +453,34 @@ export function createJobsView(deps) {
|
|
|
336
453
|
{
|
|
337
454
|
type: "fields",
|
|
338
455
|
rows: [
|
|
456
|
+
{ label: "Description", value: { $: "/main/job/description" } },
|
|
457
|
+
{ label: "Defined in", value: { $: "/main/job/definedIn" } },
|
|
458
|
+
{ label: "Owner", value: { $: "/main/job/owner" } },
|
|
339
459
|
{ label: "Schedule", value: { $: "/main/job/scheduleSummary" } },
|
|
340
460
|
{ label: "Next run", value: { $: "/main/job/nextRun" } },
|
|
341
461
|
{ label: "Last outcome", value: { $: "/main/job/lastOutcome" } },
|
|
342
462
|
{ label: "Last error", value: { $: "/main/job/lastError" } },
|
|
343
463
|
{ label: "State", value: { $: "/main/job/state" } },
|
|
344
464
|
{ label: "Version", value: { $: "/main/job/version" } },
|
|
465
|
+
{ label: "Model", value: { $: "/main/job/model" } },
|
|
466
|
+
{ label: "Thinking", value: { $: "/main/job/thinking" } },
|
|
467
|
+
{ label: "Conversation", value: { $: "/main/job/conversation" } },
|
|
468
|
+
{ label: "Deliver", value: { $: "/main/job/deliver" } },
|
|
469
|
+
{ label: "Task", value: { $: "/main/job/task" } },
|
|
345
470
|
],
|
|
346
471
|
},
|
|
472
|
+
// The action body blocks: BOTH are emitted UNCONDITIONALLY.
|
|
473
|
+
// The earlier kind-conditional form rested on a false invariant —
|
|
474
|
+
// `bumpRev` advances `rev` only (treeRev is never written), so a
|
|
475
|
+
// REST PATCH that changes a job's action kind would leave an open
|
|
476
|
+
// client showing the stale body block until reload (the clients
|
|
477
|
+
// apply fresh DATA to the SAME tree). The data-bound fix is
|
|
478
|
+
// unconditional blocks: the off-kind binding resolves null, and
|
|
479
|
+
// both clients render a null/empty markdown text as NOTHING
|
|
480
|
+
// visible (webui boundString → "" in a zero-height div; pi-tui
|
|
481
|
+
// Markdown returns zero lines for empty text).
|
|
482
|
+
{ type: "markdown", text: { $: "/main/job/prompt" } },
|
|
483
|
+
{ type: "markdown", text: { $: "/main/job/taskInputMarkdown" } },
|
|
347
484
|
],
|
|
348
485
|
}, ...(writeActions.length === 0
|
|
349
486
|
? []
|
|
@@ -355,6 +492,21 @@ export function createJobsView(deps) {
|
|
|
355
492
|
title: "Actions",
|
|
356
493
|
actions: writeActions,
|
|
357
494
|
},
|
|
495
|
+
]),
|
|
496
|
+
// Declared jobs (file:/ext:) are READ-ONLY declarations: no edit
|
|
497
|
+
// affordances here (the editor, Tasks 3-4, is api-only) — a
|
|
498
|
+
// callout points the user at the declaring source instead.
|
|
499
|
+
...(job.source === "api"
|
|
500
|
+
? []
|
|
501
|
+
: [
|
|
502
|
+
{
|
|
503
|
+
type: "callout",
|
|
504
|
+
id: "job-declared",
|
|
505
|
+
tone: "neutral",
|
|
506
|
+
title: job.source === "file"
|
|
507
|
+
? "Defined in settings.json — edit it there"
|
|
508
|
+
: `Declared by the ${extensionSlugOf(job.id)} extension`,
|
|
509
|
+
},
|
|
358
510
|
]), {
|
|
359
511
|
type: "section",
|
|
360
512
|
id: "job-runs",
|
|
@@ -479,11 +631,21 @@ export function createJobsView(deps) {
|
|
|
479
631
|
...tree,
|
|
480
632
|
root: {
|
|
481
633
|
...tree.root,
|
|
634
|
+
// The New job affordance renders for ANY identified caller
|
|
635
|
+
// (createJob admits via callerOf; the form itself is inert).
|
|
636
|
+
actions: [
|
|
637
|
+
{
|
|
638
|
+
id: "job-new",
|
|
639
|
+
label: "New job",
|
|
640
|
+
icon: "add",
|
|
641
|
+
action: { type: "navigate", params: { edit: true } },
|
|
642
|
+
},
|
|
643
|
+
],
|
|
482
644
|
selection: {
|
|
483
|
-
selected: params.job !== undefined,
|
|
645
|
+
selected: params.job !== undefined || params.edit === true,
|
|
484
646
|
back: {},
|
|
485
647
|
},
|
|
486
|
-
...(data.main.found
|
|
648
|
+
...(data.main.found || params.edit === true
|
|
487
649
|
? {}
|
|
488
650
|
: {
|
|
489
651
|
empty: {
|
|
@@ -528,7 +690,7 @@ export function createJobsView(deps) {
|
|
|
528
690
|
return { tree: noneTree(), data: {}, access: "none" };
|
|
529
691
|
}
|
|
530
692
|
const params = paramsOf(ctx.params);
|
|
531
|
-
const data = await readData(jobs, caller, params);
|
|
693
|
+
const data = await readData(jobs, caller, params, modelOptions);
|
|
532
694
|
const revisions = params.job === undefined || !data.main.found
|
|
533
695
|
? []
|
|
534
696
|
: await jobs.revisions(params.job);
|
|
@@ -556,7 +718,7 @@ export function createJobsView(deps) {
|
|
|
556
718
|
if (caller === undefined)
|
|
557
719
|
return { data: {}, access: "none" };
|
|
558
720
|
const params = paramsOf(ctx.params);
|
|
559
|
-
const data = await readData(jobs, caller, params);
|
|
721
|
+
const data = await readData(jobs, caller, params, modelOptions);
|
|
560
722
|
return {
|
|
561
723
|
// SAFETY: same JSON-mirror cast as `render`'s (the comment there).
|
|
562
724
|
data: data,
|
|
@@ -564,16 +726,53 @@ export function createJobsView(deps) {
|
|
|
564
726
|
};
|
|
565
727
|
},
|
|
566
728
|
actions: {
|
|
729
|
+
// The New job form's submit (jobs-editor Task 3): callerOf
|
|
730
|
+
// admission (anonymous non-superuser → forbidden; the userless
|
|
731
|
+
// superuser creates with an undefined owner, matching
|
|
732
|
+
// POST /v1/jobs), then the form-owned spec assembly. Field
|
|
733
|
+
// errors come back field-scoped so the channels mark the field.
|
|
734
|
+
createJob: async (ctx, payload) => {
|
|
735
|
+
const caller = callerOf(ctx);
|
|
736
|
+
if (caller === undefined) {
|
|
737
|
+
return { ok: false, error: { message: "forbidden" } };
|
|
738
|
+
}
|
|
739
|
+
const assembled = createSpecOf(formValuesOf(payload));
|
|
740
|
+
if (!assembled.ok) {
|
|
741
|
+
return {
|
|
742
|
+
ok: false,
|
|
743
|
+
error: { message: assembled.message, field: assembled.field },
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
let created;
|
|
747
|
+
try {
|
|
748
|
+
created = await jobs.createApi(assembled.spec, caller.user);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
return {
|
|
752
|
+
ok: false,
|
|
753
|
+
error: {
|
|
754
|
+
message: error instanceof Error ? error.message : String(error),
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
ok: true,
|
|
760
|
+
toast: `Created ${assembled.spec.name}`,
|
|
761
|
+
navigate: { type: "navigate", params: { job: created.id } },
|
|
762
|
+
};
|
|
763
|
+
},
|
|
567
764
|
// The sidebar row's drill-in (review Critical 2): the `edit`
|
|
568
765
|
// activation carries {itemId} = the row's id — the JOB id, the
|
|
569
766
|
// collection rows' own `id` field (the channels derive item ids
|
|
570
767
|
// from it, spec §5.5) — and the accepted result carries the
|
|
571
768
|
// navigate back (§6.3: SurfaceActOk.navigate passes the wire
|
|
572
|
-
// unchanged).
|
|
573
|
-
// result.navigate through onNavigate
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
769
|
+
// unchanged). Both channels apply it: the webui's useSurface
|
|
770
|
+
// routes result.navigate through onNavigate, and the TUI's
|
|
771
|
+
// SurfaceState does the same (jobs-editor Task 7) — its
|
|
772
|
+
// accepted-result branch fires onNavigate LAST, and the app's
|
|
773
|
+
// wiring reopens the view with the params. The compact page's
|
|
774
|
+
// Esc-back dispatch (tui/src/blocks/page.ts) is genuinely
|
|
775
|
+
// routed through the same seam.
|
|
577
776
|
openJob: async (ctx, payload) => {
|
|
578
777
|
// Same admission as callerOf (review I3's P1 remainder):
|
|
579
778
|
// anonymous → forbidden; the userless superuser is admitted.
|
|
@@ -615,6 +814,92 @@ export function createJobsView(deps) {
|
|
|
615
814
|
navigate: { type: "navigate", params: { job: jobId, revision } },
|
|
616
815
|
};
|
|
617
816
|
},
|
|
817
|
+
// The Edit form's submit (jobs-editor Task 4): the job rides
|
|
818
|
+
// the instance params (the surface is principal-scoped, so the
|
|
819
|
+
// params identify the selected job — the same shape runNow
|
|
820
|
+
// uses). The caller needs write access; a declared job is the
|
|
821
|
+
// 409-class refusal; the patch rules live in savePatchOf.
|
|
822
|
+
saveJob: async (ctx, payload) => {
|
|
823
|
+
const jobId = jobIdOf(payload) ?? paramsOf(ctx.params).job;
|
|
824
|
+
if (jobId === undefined) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
error: { message: "saveJob needs a selected job (params.job)" },
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const resolved = await actionAccess(ctx, jobId);
|
|
831
|
+
if ("error" in resolved)
|
|
832
|
+
return resolved.error;
|
|
833
|
+
if (resolved.row.source !== "api") {
|
|
834
|
+
return { ok: false, error: { message: "job_declared" } };
|
|
835
|
+
}
|
|
836
|
+
const patched = savePatchOf(formValuesOf(payload), resolved.row);
|
|
837
|
+
if (!patched.ok) {
|
|
838
|
+
return {
|
|
839
|
+
ok: false,
|
|
840
|
+
error: { message: patched.message, field: patched.field },
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const caller = callerOf(ctx);
|
|
844
|
+
try {
|
|
845
|
+
await jobs.updateApi(jobId, patched.patch, caller?.user ?? "api");
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (error instanceof JobDeclaredError ||
|
|
849
|
+
error instanceof JobNotFoundError) {
|
|
850
|
+
return { ok: false, error: { message: error.message } };
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
ok: false,
|
|
854
|
+
error: {
|
|
855
|
+
message: error instanceof Error ? error.message : String(error),
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
ok: true,
|
|
861
|
+
toast: `Saved ${patched.patch.name ?? resolved.row.name}`,
|
|
862
|
+
navigate: { type: "navigate", params: { job: jobId } },
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
// Delete: write access, api-only, then the retired job leaves
|
|
866
|
+
// the list — navigate back to the unselected view ({}).
|
|
867
|
+
deleteJob: async (ctx, payload) => {
|
|
868
|
+
const jobId = jobIdOf(payload) ?? paramsOf(ctx.params).job;
|
|
869
|
+
if (jobId === undefined) {
|
|
870
|
+
return {
|
|
871
|
+
ok: false,
|
|
872
|
+
error: { message: "deleteJob needs a selected job (params.job)" },
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const resolved = await actionAccess(ctx, jobId);
|
|
876
|
+
if ("error" in resolved)
|
|
877
|
+
return resolved.error;
|
|
878
|
+
if (resolved.row.source !== "api") {
|
|
879
|
+
return { ok: false, error: { message: "job_declared" } };
|
|
880
|
+
}
|
|
881
|
+
const caller = callerOf(ctx);
|
|
882
|
+
try {
|
|
883
|
+
await jobs.deleteApi(jobId, caller?.user ?? "api");
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
if (error instanceof JobDeclaredError ||
|
|
887
|
+
error instanceof JobNotFoundError) {
|
|
888
|
+
return { ok: false, error: { message: error.message } };
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
ok: false,
|
|
892
|
+
error: {
|
|
893
|
+
message: error instanceof Error ? error.message : String(error),
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
ok: true,
|
|
899
|
+
toast: `Deleted ${resolved.row.name}`,
|
|
900
|
+
navigate: { type: "navigate", params: {} },
|
|
901
|
+
};
|
|
902
|
+
},
|
|
618
903
|
runNow: async (ctx, payload) => {
|
|
619
904
|
const jobId = jobIdOf(payload) ?? paramsOf(ctx.params).job;
|
|
620
905
|
if (jobId === undefined) {
|
package/dist/server/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import { effectiveMethods } from "../config.js";
|
|
|
42
42
|
import { registerAuthPipeline } from "./auth/pipeline.js";
|
|
43
43
|
import { isLoopbackHost } from "./auth/loopback.js";
|
|
44
44
|
import { SseRegistry } from "./sse-registry.js";
|
|
45
|
-
import { storeAgentChange } from "./helpers.js";
|
|
45
|
+
import { listModels, storeAgentChange } from "./helpers.js";
|
|
46
46
|
import { registerCoreOwnershipParticipant } from "./ownership.js";
|
|
47
47
|
import { createJobsView } from "./core-views/jobs.js";
|
|
48
48
|
import { createSkillsView } from "./core-views/skills.js";
|
|
@@ -324,6 +324,7 @@ export async function createServer(opened, config, contributedBundles = [], opti
|
|
|
324
324
|
createJobsView({
|
|
325
325
|
jobs: opened.jobs,
|
|
326
326
|
now: () => new Date(),
|
|
327
|
+
models: () => listModels(opened).map((m) => ({ id: m.id, label: m.name })),
|
|
327
328
|
accessPrincipal: (principal) => accessPrincipalOf(principal, opened.identity.users),
|
|
328
329
|
}),
|
|
329
330
|
createSkillsView({
|
|
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
## [0.10.0] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
- `JobSpec.description?: string` (and the flattened `JobWire.description`): the optional,
|
|
12
|
+
trimmed, ≤500-char human description shown in the Jobs view.
|
|
13
|
+
|
|
9
14
|
## [0.9.0] - 2026-11-05
|
|
10
15
|
|
|
11
16
|
- `InteractionRecord` gains optional `declinable: true` and settled `declinedVia:
|
|
@@ -53,6 +53,8 @@ export type JobAction = {
|
|
|
53
53
|
/** A job's declared shape — what every source (api/file/extension) supplies. */
|
|
54
54
|
export type JobSpec = {
|
|
55
55
|
name: string;
|
|
56
|
+
/** Optional ≤500-char human description, shown in the Jobs view. */
|
|
57
|
+
description?: string;
|
|
56
58
|
schedule: JobSchedule;
|
|
57
59
|
action: JobAction;
|
|
58
60
|
overlap?: "skip" | "queue";
|
|
@@ -68,6 +70,8 @@ export type JobWire = {
|
|
|
68
70
|
source: "api" | "file" | "extension";
|
|
69
71
|
owner?: string;
|
|
70
72
|
sharing?: Sharing;
|
|
73
|
+
/** Optional ≤500-char human description, shown in the Jobs view. */
|
|
74
|
+
description?: string;
|
|
71
75
|
schedule: JobSchedule;
|
|
72
76
|
action: JobAction;
|
|
73
77
|
enabled: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/sigrun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Self-hosted, multi-user personal-assistant server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@earendil-works/pi-tui": "^1.3.0",
|
|
46
46
|
"@fastify/static": "^10.1.7",
|
|
47
47
|
"@sigrun/ext-codemode": "0.1.2",
|
|
48
|
-
"@sigrun/ext-curator": "0.3.
|
|
49
|
-
"@sigrun/ext-gateway": "0.1.
|
|
50
|
-
"@sigrun/ext-mcp": "0.2.
|
|
48
|
+
"@sigrun/ext-curator": "0.3.2",
|
|
49
|
+
"@sigrun/ext-gateway": "0.1.19",
|
|
50
|
+
"@sigrun/ext-mcp": "0.2.3",
|
|
51
51
|
"@sigrun/ext-tool-search": "0.1.2",
|
|
52
52
|
"@sigrun/oauth": "0.1.1",
|
|
53
|
-
"@sigrun/shared": "0.
|
|
53
|
+
"@sigrun/shared": "0.10.0",
|
|
54
54
|
"croner": "^10.0.1",
|
|
55
55
|
"fastify": "^5.12.7",
|
|
56
56
|
"semver": "^7.8.5",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"@sigrun/shared"
|
|
62
62
|
],
|
|
63
63
|
"optionalDependencies": {
|
|
64
|
-
"@sigrun/sandbox-linux-arm64": "0.
|
|
65
|
-
"@sigrun/sandbox-linux-x64": "0.
|
|
66
|
-
"@sigrun/tui": "0.
|
|
64
|
+
"@sigrun/sandbox-linux-arm64": "0.10.0",
|
|
65
|
+
"@sigrun/sandbox-linux-x64": "0.10.0",
|
|
66
|
+
"@sigrun/tui": "0.10.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@sigrun/memory-markdown": "file:../sigrun-memory-markdown",
|