@sigrun/ext-curator 0.0.0-stage → 0.1.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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The targets step (`src/steps/targets.ts`, Task B4 — loop 2 step
3
+ * `targets`, the pass's LAST step): run each maintain-capable curation
4
+ * target's maintenance hook through `host.curation.maintain`, handing
5
+ * it the tick's deadline and the REMAINING daily digest runs (the
6
+ * budget the five model steps did not spend — the brief: "within the
7
+ * remaining budget").
8
+ *
9
+ * The step itself spends nothing — the target's own `run` calls are
10
+ * budgeted by the `runs` count it receives (one digest run each, per
11
+ * the P6 contract), and a target that reports `more` keeps the rest for
12
+ * the next pass. A failing target is logged and never blocks the others.
13
+ */
14
+ import { type ModelStepDeps } from "./run.js";
15
+ /** The loop-2 targets step: maintain every maintain-capable target. */
16
+ export declare function maintainTargets(deps: ModelStepDeps): Promise<boolean>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The targets step (`src/steps/targets.ts`, Task B4 — loop 2 step
3
+ * `targets`, the pass's LAST step): run each maintain-capable curation
4
+ * target's maintenance hook through `host.curation.maintain`, handing
5
+ * it the tick's deadline and the REMAINING daily digest runs (the
6
+ * budget the five model steps did not spend — the brief: "within the
7
+ * remaining budget").
8
+ *
9
+ * The step itself spends nothing — the target's own `run` calls are
10
+ * budgeted by the `runs` count it receives (one digest run each, per
11
+ * the P6 contract), and a target that reports `more` keeps the rest for
12
+ * the next pass. A failing target is logged and never blocks the others.
13
+ */
14
+ import { stepLog } from "./run.js";
15
+ /** The loop-2 targets step: maintain every maintain-capable target. */
16
+ export async function maintainTargets(deps) {
17
+ const { host, user } = deps;
18
+ const log = stepLog(deps);
19
+ if (deps.budget.left() < 1)
20
+ return false; // nothing to hand a target: stop
21
+ const targets = await host.curation.targets(user);
22
+ for (const target of targets) {
23
+ if (!target.maintain)
24
+ continue;
25
+ if (deps.now().getTime() >= deps.deadlineAt)
26
+ return false;
27
+ await host.curation
28
+ .maintain(user, target.id, {
29
+ deadline: deps.deadlineAt,
30
+ runs: deps.budget.left(),
31
+ })
32
+ .then((report) => {
33
+ if (report.more) {
34
+ log(`loop 2: target ${target.id} stopped early — the next pass continues it`);
35
+ }
36
+ })
37
+ .catch((error) => {
38
+ log(`loop 2: ${user} maintain of target ${target.id} failed: ${error instanceof Error ? error.message : String(error)}`);
39
+ });
40
+ }
41
+ return true;
42
+ }
package/dist/view.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The curator view (`src/view.ts`, Task B5): the per-user surface the
3
+ * notice navigates to — learned skills (name, description, last used,
4
+ * pin toggle, archive/restore), overlays (orphaned flag), the change
5
+ * feed (newest 50) grouped by review with an **Undo review** action,
6
+ * and a pause switch.
7
+ *
8
+ * Access follows `accessOf` (spec P8: "the user, plus the superuser as
9
+ * with conversations"): the resource is the caller's OWN curation data,
10
+ * so a signed-in user is its owner ("write") and an anonymous caller
11
+ * has no instance at all ("none" — accessOf's ownerless rule would
12
+ * otherwise widen to household write, so the anonymous case is gated by
13
+ * hand). Every render and action resolves the CALLING principal's own
14
+ * stores — bob's instance reads bob's data, never alice's — and the
15
+ * per-principal `instanceKey` keeps two users' views apart as surface
16
+ * instances.
17
+ *
18
+ * The actions (what an ambient record can never carry — a view's action
19
+ * routes DO reach the extension). Per-item actions ride the block
20
+ * set's item-scoped dispatches, which carry `{itemId}` (a card action
21
+ * button's submit dispatches a `null` payload — it cannot name an
22
+ * item, `onCheck`/`onActivate` can):
23
+ *
24
+ * - `pin` — the skills collection's `checkable`/`onCheck`:
25
+ * `{itemId: skill, value}` → the skill store's `pin`;
26
+ * - `toggleArchive` — the skills collection's `onActivate`
27
+ * (`{itemId: skill}`): reads the store's current state and archives
28
+ * or restores through `apply` (feed-recorded, undoable);
29
+ * - `undoReview` — the feed collection's `onActivate`
30
+ * (`{itemId: reviewId}`): `host.curation.undoReview`, then
31
+ * `host.clearAmbient` on the notice conversation's `curator.notice`
32
+ * slot (the notice ledger in `src/notices.ts` remembers it);
33
+ * - `setPaused` — the page header's pause button (a submit dispatching
34
+ * `{}`): toggles the user's `paused` setting, read live by both
35
+ * loops — pausing takes effect on the next event, restart-free.
36
+ *
37
+ * The tree is data-driven where the block set binds (item templates
38
+ * over literal rows); the whole tree passes the server's own
39
+ * `validateTree` (pinned in the tests) because the server runs that
40
+ * exact walk before serving.
41
+ */
42
+ import type { CurationFeedEntry, Principal, SigrunHost, SurfaceDef } from "@sigrun/sigrun/extension";
43
+ import type { Access } from "@sigrun/shared/access";
44
+ /** How many feed entries the view lists (the brief: newest 50). */
45
+ export declare const VIEW_FEED_LIMIT = 50;
46
+ /** One grouped review block in the feed (newest first). */
47
+ export interface ReviewGroup {
48
+ readonly id: string;
49
+ readonly at: string;
50
+ readonly entries: readonly {
51
+ readonly id: string;
52
+ readonly skill: string;
53
+ readonly action: string;
54
+ readonly at: string;
55
+ readonly reason?: string;
56
+ }[];
57
+ }
58
+ /** The view's data document (everything the tree binds against). */
59
+ export interface CuratorViewData {
60
+ readonly user: string;
61
+ readonly paused: boolean;
62
+ readonly skills: readonly {
63
+ readonly id: string;
64
+ readonly name: string;
65
+ readonly description: string;
66
+ readonly archived: boolean;
67
+ readonly pinned: boolean;
68
+ readonly lastUsedLabel: string;
69
+ }[];
70
+ readonly overlays: readonly {
71
+ readonly id: string;
72
+ readonly skill: string;
73
+ readonly root: string;
74
+ readonly orphaned: boolean;
75
+ }[];
76
+ readonly reviews: readonly ReviewGroup[];
77
+ }
78
+ /**
79
+ * Resolve one principal's access to their per-user view instance (spec
80
+ * P8: "the user, plus the superuser as with conversations"): the
81
+ * resource is the principal's OWN curation data, so a signed-in user is
82
+ * its owner (write) and the service principal the superuser (write);
83
+ * an anonymous caller has no instance at all — accessOf's ownerless
84
+ * rule would widen to household write, so anonymous is none BY HAND.
85
+ */
86
+ export declare function viewAccess(principal: Principal, user: string): Access;
87
+ /** Group a feed by review: entries sharing a reviewId form one group
88
+ * (the feed's newest-first order preserved, outside and inside). An
89
+ * entry with no reviewId (user pins, direct edits) groups alone. */
90
+ export declare function groupFeed(feed: readonly CurationFeedEntry[]): readonly ReviewGroup[];
91
+ /** The view surface (`SurfaceDef<"view">`), built over the host. */
92
+ export declare function createCuratorView(host: SigrunHost): SurfaceDef<"view">;
package/dist/view.js ADDED
@@ -0,0 +1,405 @@
1
+ /**
2
+ * The curator view (`src/view.ts`, Task B5): the per-user surface the
3
+ * notice navigates to — learned skills (name, description, last used,
4
+ * pin toggle, archive/restore), overlays (orphaned flag), the change
5
+ * feed (newest 50) grouped by review with an **Undo review** action,
6
+ * and a pause switch.
7
+ *
8
+ * Access follows `accessOf` (spec P8: "the user, plus the superuser as
9
+ * with conversations"): the resource is the caller's OWN curation data,
10
+ * so a signed-in user is its owner ("write") and an anonymous caller
11
+ * has no instance at all ("none" — accessOf's ownerless rule would
12
+ * otherwise widen to household write, so the anonymous case is gated by
13
+ * hand). Every render and action resolves the CALLING principal's own
14
+ * stores — bob's instance reads bob's data, never alice's — and the
15
+ * per-principal `instanceKey` keeps two users' views apart as surface
16
+ * instances.
17
+ *
18
+ * The actions (what an ambient record can never carry — a view's action
19
+ * routes DO reach the extension). Per-item actions ride the block
20
+ * set's item-scoped dispatches, which carry `{itemId}` (a card action
21
+ * button's submit dispatches a `null` payload — it cannot name an
22
+ * item, `onCheck`/`onActivate` can):
23
+ *
24
+ * - `pin` — the skills collection's `checkable`/`onCheck`:
25
+ * `{itemId: skill, value}` → the skill store's `pin`;
26
+ * - `toggleArchive` — the skills collection's `onActivate`
27
+ * (`{itemId: skill}`): reads the store's current state and archives
28
+ * or restores through `apply` (feed-recorded, undoable);
29
+ * - `undoReview` — the feed collection's `onActivate`
30
+ * (`{itemId: reviewId}`): `host.curation.undoReview`, then
31
+ * `host.clearAmbient` on the notice conversation's `curator.notice`
32
+ * slot (the notice ledger in `src/notices.ts` remembers it);
33
+ * - `setPaused` — the page header's pause button (a submit dispatching
34
+ * `{}`): toggles the user's `paused` setting, read live by both
35
+ * loops — pausing takes effect on the next event, restart-free.
36
+ *
37
+ * The tree is data-driven where the block set binds (item templates
38
+ * over literal rows); the whole tree passes the server's own
39
+ * `validateTree` (pinned in the tests) because the server runs that
40
+ * exact walk before serving.
41
+ */
42
+ import { accessOf } from "@sigrun/shared/access";
43
+ import { readNotices } from "./notices.js";
44
+ import { CURATOR_NOTICE_KIND } from "./notices.js";
45
+ import { parseCuratorSettings } from "./settings.js";
46
+ /** How many feed entries the view lists (the brief: newest 50). */
47
+ export const VIEW_FEED_LIMIT = 50;
48
+ /** The zero-signal context the ambient clear runs under. */
49
+ const CONTEXT = { abortSignal: undefined };
50
+ /**
51
+ * Resolve one principal's access to their per-user view instance (spec
52
+ * P8: "the user, plus the superuser as with conversations"): the
53
+ * resource is the principal's OWN curation data, so a signed-in user is
54
+ * its owner (write) and the service principal the superuser (write);
55
+ * an anonymous caller has no instance at all — accessOf's ownerless
56
+ * rule would widen to household write, so anonymous is none BY HAND.
57
+ */
58
+ export function viewAccess(principal, user) {
59
+ if (principal.user === undefined)
60
+ return "none";
61
+ return accessOf({ user: principal.user, superuser: principal.service }, { owner: user });
62
+ }
63
+ /** Group a feed by review: entries sharing a reviewId form one group
64
+ * (the feed's newest-first order preserved, outside and inside). An
65
+ * entry with no reviewId (user pins, direct edits) groups alone. */
66
+ export function groupFeed(feed) {
67
+ const groups = new Map();
68
+ for (const entry of feed) {
69
+ const key = entry.reviewId ?? entry.id;
70
+ const row = {
71
+ id: entry.id,
72
+ skill: entry.skill,
73
+ action: entry.action,
74
+ at: entry.at,
75
+ ...(entry.reason === undefined ? {} : { reason: entry.reason }),
76
+ };
77
+ const existing = groups.get(key);
78
+ if (existing === undefined) {
79
+ groups.set(key, { id: key, at: entry.at, entries: [row] });
80
+ }
81
+ else {
82
+ groups.set(key, { ...existing, entries: [...existing.entries, row] });
83
+ }
84
+ }
85
+ return [...groups.values()];
86
+ }
87
+ /** A human label for one skill's last-used stamp ("never" or the date). */
88
+ function lastUsedLabel(at) {
89
+ if (at === undefined)
90
+ return "never";
91
+ const date = new Date(at);
92
+ return Number.isNaN(date.getTime()) ? "never" : date.toISOString().slice(0, 10);
93
+ }
94
+ /** Build the view's data document for one user (the render's model). */
95
+ async function readData(host, user) {
96
+ const settings = parseCuratorSettings(await host.stores.user(user).settings.get());
97
+ const usage = await host.curation.usage(user);
98
+ const skills = await host.curation.skills(user).list();
99
+ const overlays = await host.curation.overlays(user).list();
100
+ const feed = await host.curation.skills(user).feed(VIEW_FEED_LIMIT);
101
+ return {
102
+ user,
103
+ paused: settings.paused,
104
+ skills: [...skills]
105
+ .sort((a, b) => a.name.localeCompare(b.name))
106
+ .map((row) => ({
107
+ id: row.name,
108
+ name: row.name,
109
+ description: row.description,
110
+ archived: row.archived,
111
+ pinned: row.pinned,
112
+ lastUsedLabel: lastUsedLabel(usage[row.name]),
113
+ })),
114
+ overlays: [...overlays]
115
+ .map((row) => ({
116
+ id: row.key.skill,
117
+ skill: row.key.skill,
118
+ root: row.key.root,
119
+ orphaned: row.orphanedSince !== undefined,
120
+ }))
121
+ .sort((a, b) => a.skill.localeCompare(b.skill)),
122
+ reviews: groupFeed(feed),
123
+ };
124
+ }
125
+ /** Whether `payload` (an action's JSON argument) is a plain object. */
126
+ function isObject(payload) {
127
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
128
+ }
129
+ /** The `{itemId, value}` dispatch an `onCheck` toggle carries. */
130
+ function itemToggle(payload) {
131
+ if (!isObject(payload))
132
+ return undefined;
133
+ const itemId = payload.itemId;
134
+ const value = payload.value;
135
+ if (typeof itemId !== "string" || itemId === "")
136
+ return undefined;
137
+ if (typeof value !== "boolean")
138
+ return undefined;
139
+ return { itemId, value };
140
+ }
141
+ /** The `{itemId}` dispatch an `onActivate` click carries. */
142
+ function itemIdOf(payload) {
143
+ if (!isObject(payload))
144
+ return undefined;
145
+ const itemId = payload.itemId;
146
+ return typeof itemId === "string" && itemId !== "" ? itemId : undefined;
147
+ }
148
+ /** The view surface (`SurfaceDef<"view">`), built over the host. */
149
+ export function createCuratorView(host) {
150
+ /** Reject for a caller with no instance of their own. */
151
+ const noAccess = () => ({
152
+ ok: false,
153
+ error: { message: "sign in to use the curator view" },
154
+ });
155
+ /** The caller's user, or a reject when they have no instance. */
156
+ function callerOf(ctx) {
157
+ const user = ctx.principal.user;
158
+ if (user === undefined)
159
+ return { reject: noAccess() };
160
+ if (viewAccess(ctx.principal, user) === "none")
161
+ return { reject: noAccess() };
162
+ return { user };
163
+ }
164
+ return {
165
+ id: "curator",
166
+ title: "Curator",
167
+ icon: "book",
168
+ placement: "view",
169
+ // A principal-owned surface (§7.3): the principal folds into the
170
+ // instance identity so two users' views are two instances.
171
+ instanceKey: (params, principal) => JSON.stringify([principal.user ?? "", params]),
172
+ render: async (ctx) => {
173
+ const who = callerOf(ctx);
174
+ if ("reject" in who) {
175
+ return { tree: emptyTree(), data: {}, access: "none" };
176
+ }
177
+ const data = await readData(host, who.user);
178
+ return {
179
+ tree: viewTree(data),
180
+ // SAFETY: the data document is a JSON mirror (every field a
181
+ // literal); the interface lacks the index signature JsonObject
182
+ // requires, so the cast only restores that erased identity.
183
+ data: data,
184
+ access: "write",
185
+ };
186
+ },
187
+ data: async (ctx) => {
188
+ const who = callerOf(ctx);
189
+ if ("reject" in who)
190
+ return { data: {}, access: "none" };
191
+ return {
192
+ // SAFETY: same JSON-mirror cast as `render`'s (the comment there).
193
+ data: (await readData(host, who.user)),
194
+ access: "write",
195
+ };
196
+ },
197
+ actions: {
198
+ undoReview: async (ctx, payload) => {
199
+ const who = callerOf(ctx);
200
+ if ("reject" in who)
201
+ return who.reject;
202
+ const reviewId = itemIdOf(payload);
203
+ if (reviewId === undefined) {
204
+ return { ok: false, error: { message: "undoReview expects {itemId}" } };
205
+ }
206
+ const reverted = await host.curation.undoReview(who.user, reviewId);
207
+ // Clear the notice this review set, when the ledger remembers
208
+ // one (a digest review with no notice clears nothing).
209
+ const notice = (await readNotices(host, who.user)).find((entry) => entry.reviewId === reviewId);
210
+ if (notice !== undefined) {
211
+ // The revert already succeeded — a failing clear only leaves a
212
+ // stale notice (replaced by the next review's), so it must
213
+ // never turn the undo into a failed action with no toast.
214
+ await host.clearAmbient(notice.conversationId, CURATOR_NOTICE_KIND, CONTEXT).catch(() => undefined);
215
+ }
216
+ return {
217
+ ok: true,
218
+ toast: reverted > 0 ? `Undid ${reverted} change(s)` : "Nothing to undo",
219
+ };
220
+ },
221
+ pin: async (ctx, payload) => {
222
+ const who = callerOf(ctx);
223
+ if ("reject" in who)
224
+ return who.reject;
225
+ const toggle = itemToggle(payload);
226
+ if (toggle === undefined) {
227
+ return { ok: false, error: { message: "pin expects {itemId, value}" } };
228
+ }
229
+ await host.curation.skills(who.user).pin(toggle.itemId, toggle.value);
230
+ return {
231
+ ok: true,
232
+ toast: toggle.value ? `Pinned ${toggle.itemId}` : `Unpinned ${toggle.itemId}`,
233
+ };
234
+ },
235
+ toggleArchive: async (ctx, payload) => {
236
+ const who = callerOf(ctx);
237
+ if ("reject" in who)
238
+ return who.reject;
239
+ const name = itemIdOf(payload);
240
+ if (name === undefined) {
241
+ return { ok: false, error: { message: "toggleArchive expects {itemId}" } };
242
+ }
243
+ // The current store state decides the direction: click an
244
+ // active skill → archive; click an archived one → restore.
245
+ const row = (await host.curation.skills(who.user).list()).find((skill) => skill.name === name);
246
+ if (row === undefined) {
247
+ return { ok: false, error: { message: `unknown skill: ${name}` } };
248
+ }
249
+ const change = row.archived
250
+ ? { kind: "restore" }
251
+ : { kind: "archive" };
252
+ await host.curation
253
+ .skills(who.user)
254
+ .apply(name, change, { reason: `${change.kind === "archive" ? "archived" : "restored"} from the curator view` });
255
+ return {
256
+ ok: true,
257
+ toast: change.kind === "archive" ? `Archived ${name}` : `Restored ${name}`,
258
+ };
259
+ },
260
+ setPaused: async (ctx, payload) => {
261
+ const who = callerOf(ctx);
262
+ if ("reject" in who)
263
+ return who.reject;
264
+ // The header button dispatches `{}` (toggle); a form-shaped
265
+ // `{values: {paused}}` or bare `{paused}` sets explicitly.
266
+ const explicit = (isObject(payload) && isObject(payload.values)
267
+ ? payload.values.paused
268
+ : undefined) ??
269
+ (isObject(payload) ? payload.paused : undefined);
270
+ const raw = await host.stores.user(who.user).settings.get();
271
+ const current = parseCuratorSettings(raw).paused;
272
+ const paused = typeof explicit === "boolean" ? explicit : !current;
273
+ if (paused === current) {
274
+ return { ok: true, toast: paused ? "Curation paused" : "Curation resumed" };
275
+ }
276
+ await host.stores.user(who.user).settings.set({ ...raw, paused });
277
+ return {
278
+ ok: true,
279
+ toast: paused ? "Curation paused" : "Curation resumed",
280
+ };
281
+ },
282
+ },
283
+ };
284
+ }
285
+ /** The signed-out/none-access tree (an empty page; the route 404s it). */
286
+ function emptyTree() {
287
+ return {
288
+ v: 1,
289
+ root: {
290
+ type: "page",
291
+ title: "Curator",
292
+ main: [
293
+ { type: "markdown", text: "Sign in to see your curator activity." },
294
+ ],
295
+ },
296
+ };
297
+ }
298
+ /** The view tree: header with the pause switch, skills, overlays, feed. */
299
+ function viewTree(data) {
300
+ return {
301
+ v: 1,
302
+ root: {
303
+ type: "page",
304
+ id: "curator-root",
305
+ title: "Curator",
306
+ subtitle: `Learned activity for ${data.user}${data.paused ? " (paused)" : ""}`,
307
+ actions: [
308
+ {
309
+ id: "pause-switch",
310
+ label: data.paused ? "Resume curation" : "Pause curation",
311
+ icon: data.paused ? "sun" : "moon",
312
+ action: { type: "submit", name: "setPaused" },
313
+ },
314
+ ],
315
+ main: [
316
+ {
317
+ type: "section",
318
+ id: "skills-section",
319
+ title: "Learned skills",
320
+ children: [
321
+ {
322
+ type: "markdown",
323
+ text: "Toggle the checkbox to pin or unpin. Click a skill to archive it; click an archived one to restore.",
324
+ },
325
+ {
326
+ type: "collection",
327
+ id: "skills",
328
+ display: "list",
329
+ items: data.skills,
330
+ checkable: "pinned",
331
+ onCheck: "pin",
332
+ onActivate: { type: "edit", name: "toggleArchive" },
333
+ item: {
334
+ title: { $: "./name" },
335
+ subtitle: { $: "./description" },
336
+ body: { $: "./lastUsedLabel" },
337
+ badges: [
338
+ { text: "pinned", tone: "good" },
339
+ { text: "archived", tone: "neutral" },
340
+ ],
341
+ },
342
+ },
343
+ ],
344
+ },
345
+ {
346
+ type: "section",
347
+ id: "overlays-section",
348
+ title: "Overlays",
349
+ children: [
350
+ data.overlays.length === 0
351
+ ? {
352
+ type: "markdown",
353
+ text: "No overlays on upstream skills.",
354
+ }
355
+ : {
356
+ type: "collection",
357
+ id: "overlays",
358
+ display: "list",
359
+ items: data.overlays,
360
+ item: {
361
+ title: { $: "./skill" },
362
+ subtitle: { $: "./root" },
363
+ badges: [{ text: "orphaned", tone: "warn" }],
364
+ },
365
+ },
366
+ ],
367
+ },
368
+ {
369
+ type: "section",
370
+ id: "feed-section",
371
+ title: "Change feed (newest 50, grouped by review)",
372
+ children: [
373
+ {
374
+ type: "markdown",
375
+ text: "Click a review to undo every change it made.",
376
+ },
377
+ data.reviews.length === 0
378
+ ? {
379
+ type: "markdown",
380
+ text: "Nothing learned yet. Use /learn to ask for a review now.",
381
+ }
382
+ : {
383
+ type: "collection",
384
+ id: "reviews",
385
+ display: "list",
386
+ items: data.reviews.map((group) => ({
387
+ id: group.id,
388
+ at: group.at,
389
+ summary: group.entries
390
+ .map((entry) => `${entry.action} ${entry.skill}`)
391
+ .join(", "),
392
+ })),
393
+ onActivate: { type: "edit", name: "undoReview" },
394
+ item: {
395
+ title: { $: "./id" },
396
+ subtitle: { $: "./summary" },
397
+ meta: [{ time: { $: "./at" }, format: "relative" }],
398
+ },
399
+ },
400
+ ],
401
+ },
402
+ ],
403
+ },
404
+ };
405
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "@sigrun/ext-curator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/ext-curator",
3
+ "version": "0.1.0",
4
+ "description": "Curator extension for sigrun (pi-durable): the per-user fork review and idle maintenance pass over learned skills and curation targets",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node scripts/check-manifest.mjs --expect-name @sigrun/ext-curator && vitest run",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "@earendil-works/chord": "1.3.0 || 1.4.0",
29
+ "@earendil-works/pi-ai": "1.3.0 || 1.4.0",
30
+ "@earendil-works/pi-durable": "1.3.0 || 1.4.0"
31
+ },
32
+ "devDependencies": {
33
+ "@earendil-works/chord": "1.4.0",
34
+ "@earendil-works/pi-ai": "1.4.0",
35
+ "@earendil-works/pi-durable": "1.4.0",
36
+ "@sigrun/sigrun": "file:../sigrun",
37
+ "@sigrun/shared": "file:../sigrun/shared",
38
+ "@types/node": "^26.6.4",
39
+ "tsx": "^4.23.15",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^5.0.3"
42
+ },
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "github",
46
+ "url": "git+https://github.com/sigrun-ai/sigrun-ext-curator.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/sigrun-ai/sigrun-ext-curator/issues"
50
+ },
51
+ "homepage": "https://github.com/sigrun-ai/sigrun-ext-curator#readme",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "provenance": true
55
+ }
56
+ }