@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.
@@ -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({ id: row.id, name: row.name, ...summaryFields(row) });
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: { $: "./scheduleSummary" },
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
- if (data.main.found && data.main.job !== null) {
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). The webui applies it (useSurface routes
573
- // result.navigate through onNavigate); the TUI does NOT apply
574
- // result.navigate yet — a PRE-EXISTING channel gap for every
575
- // ext view's action-result navigation, out of scope here
576
- // (whole-branch review ledger).
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) {
@@ -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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigrun/shared",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "sigrun shared leaf package \u2014 theme tokens, turn/conversation shaping, and the channel wire types (pure leaf, zero dependencies)",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigrun/sigrun",
3
- "version": "0.9.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.0",
49
- "@sigrun/ext-gateway": "0.1.18",
50
- "@sigrun/ext-mcp": "0.2.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.9.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.9.0",
65
- "@sigrun/sandbox-linux-x64": "0.9.0",
66
- "@sigrun/tui": "0.9.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",