@nema-project/nema 0.1.4 → 0.1.6

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.
@@ -32,12 +32,49 @@ var Implementation = z.object({
32
32
  });
33
33
 
34
34
  // ../../../packages/shared/dist/entry.js
35
+ import { z as z3 } from "zod";
36
+
37
+ // ../../../packages/shared/dist/sourceEvidence.js
35
38
  import { z as z2 } from "zod";
39
+ var NoControls = /^[^\x00-\x1f\x7f]*$/;
40
+ var SourceRevision = z2.string().min(1).max(256).regex(NoControls).nullable();
41
+ var SnapshotHash = z2.string().regex(/^[0-9a-f]{64}$/);
42
+ var SourceExcerpt = z2.object({
43
+ locator: z2.string().min(1).max(200),
44
+ text: z2.string().min(1).max(2e3)
45
+ });
46
+ var SourceSnapshot = z2.object({
47
+ schemaVersion: z2.literal(1),
48
+ provider: z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
49
+ resourceId: z2.string().min(1).max(512).regex(NoControls).refine((value) => value.trim() === value, "resourceId must not have surrounding whitespace"),
50
+ revision: SourceRevision,
51
+ fetchedAt: z2.iso.datetime({ offset: true }),
52
+ title: z2.string().min(1).max(200),
53
+ canonicalUrl: z2.string().refine((value) => {
54
+ try {
55
+ const url = new URL(value);
56
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
57
+ } catch {
58
+ return false;
59
+ }
60
+ }, "canonicalUrl must be http or https without user information").nullable(),
61
+ contentHash: SnapshotHash,
62
+ excerpts: z2.array(SourceExcerpt).min(1).max(20).refine((excerpts) => excerpts.reduce((total, excerpt) => total + excerpt.text.length, 0) <= 8e3, "excerpt text must total at most 8000 characters")
63
+ });
64
+ var SourceRef = z2.object({
65
+ sourceEntryId: z2.string().regex(/^S-[1-9]\d*$/),
66
+ revision: SourceRevision,
67
+ snapshotHash: SnapshotHash,
68
+ locator: z2.string().min(1).max(200).optional(),
69
+ quote: z2.string().min(1).max(500).optional()
70
+ }).refine((ref) => ref.quote === void 0 || ref.locator !== void 0, { path: ["locator"], message: "locator is required with quote" });
71
+
72
+ // ../../../packages/shared/dist/entry.js
36
73
  var ENTRY_KINDS = ["requirement", "decision", "tradeoff", "question", "source", "diagram", "workpackage"];
37
- var EntryKindSchema = z2.enum(ENTRY_KINDS);
74
+ var EntryKindSchema = z3.enum(ENTRY_KINDS);
38
75
  var BASIS_VALUES = ["stated", "decided", "inferred"];
39
- var BasisSchema = z2.enum(BASIS_VALUES);
40
- var Quote = z2.object({ messageId: z2.string().min(1), text: z2.string().min(1).max(500) });
76
+ var BasisSchema = z3.enum(BASIS_VALUES);
77
+ var Quote = z3.object({ messageId: z3.string().min(1), text: z3.string().min(1).max(500) });
41
78
  var DIAGRAM_TYPES = [
42
79
  "architecture",
43
80
  "workflow",
@@ -50,34 +87,34 @@ var DIAGRAM_TYPES = [
50
87
  "timeline",
51
88
  "waterfall"
52
89
  ];
53
- var DiagramMeta = z2.object({
54
- repoId: z2.string().min(1),
55
- type: z2.enum(DIAGRAM_TYPES),
90
+ var DiagramMeta = z3.object({
91
+ repoId: z3.string().min(1),
92
+ type: z3.enum(DIAGRAM_TYPES),
56
93
  /** repo id -> main sha the diagram was drawn at. */
57
- sourceCommits: z2.record(z2.string(), z2.string()),
94
+ sourceCommits: z3.record(z3.string(), z3.string()),
58
95
  /** archify node id -> repo-relative paths that node stands for. */
59
- nodePaths: z2.record(z2.string(), z2.array(z2.string()))
96
+ nodePaths: z3.record(z3.string(), z3.array(z3.string()))
60
97
  });
61
98
  var MAX_WP_TASKS = 15;
62
- var WpTask = z2.object({
63
- n: z2.number().int().min(1).max(MAX_WP_TASKS),
64
- title: z2.string(),
65
- satisfies: z2.array(z2.string()),
66
- files: z2.array(z2.object({ op: z2.enum(["modify", "create"]), repo: z2.string(), path: z2.string() })),
67
- status: z2.enum(["pending", "reported", "verified", "mismatch"]),
68
- inferred: z2.boolean(),
69
- reportMessageId: z2.string().nullable(),
70
- confirmedBy: z2.literal("user").nullable()
99
+ var WpTask = z3.object({
100
+ n: z3.number().int().min(1).max(MAX_WP_TASKS),
101
+ title: z3.string(),
102
+ satisfies: z3.array(z3.string()),
103
+ files: z3.array(z3.object({ op: z3.enum(["modify", "create"]), repo: z3.string(), path: z3.string() })),
104
+ status: z3.enum(["pending", "reported", "verified", "mismatch"]),
105
+ inferred: z3.boolean(),
106
+ reportMessageId: z3.string().nullable(),
107
+ confirmedBy: z3.literal("user").nullable()
71
108
  });
72
- var WorkPackageMeta = z2.object({
73
- requirements: z2.array(z2.string()),
74
- repos: z2.array(z2.string()),
75
- approvedAt: z2.string().nullable(),
76
- approvedCommit: z2.string().nullable(),
77
- blockedOn: z2.string().nullable(),
78
- tasks: z2.array(WpTask)
109
+ var WorkPackageMeta = z3.object({
110
+ requirements: z3.array(z3.string()),
111
+ repos: z3.array(z3.string()),
112
+ approvedAt: z3.string().nullable(),
113
+ approvedCommit: z3.string().nullable(),
114
+ blockedOn: z3.string().nullable(),
115
+ tasks: z3.array(WpTask)
79
116
  });
80
- var IsoDateTime = z2.iso.datetime({ offset: true }).nullable();
117
+ var IsoDateTime = z3.iso.datetime({ offset: true }).nullable();
81
118
  var PREFIX_KIND = {
82
119
  R: "requirement",
83
120
  D: "decision",
@@ -103,9 +140,9 @@ var KIND_STATUSES = {
103
140
  workpackage: ["draft", "approved", "in_progress", "done", "dropped"]
104
141
  };
105
142
  var ALL_STATUSES = ["active", "superseded", "dropped", "open", "resolved", "draft", "approved", "in_progress", "done"];
106
- var StatusSchema = z2.enum(ALL_STATUSES);
107
- var BusinessDate = z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable();
108
- var HttpUrl = z2.string().refine((s) => {
143
+ var StatusSchema = z3.enum(ALL_STATUSES);
144
+ var BusinessDate = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable();
145
+ var HttpUrl = z3.string().refine((s) => {
109
146
  try {
110
147
  const p = new URL(s).protocol;
111
148
  return p === "http:" || p === "https:";
@@ -113,17 +150,23 @@ var HttpUrl = z2.string().refine((s) => {
113
150
  return false;
114
151
  }
115
152
  }, "url must be http or https");
116
- var Priority = z2.union([z2.literal(1), z2.literal(2), z2.literal(3)]);
153
+ var Priority = z3.union([z3.literal(1), z3.literal(2), z3.literal(3)]);
117
154
  function refineKind(v, ctx) {
118
155
  if (v.status !== void 0 && !KIND_STATUSES[v.kind].includes(v.status)) {
119
156
  ctx.addIssue({ code: "custom", path: ["status"], message: `invalid status "${v.status}" for ${v.kind}` });
120
157
  }
121
158
  if (v.kind === "source") {
122
- if (v.url === void 0)
159
+ if (v.url === void 0 && v.sourceSnapshot === void 0)
123
160
  ctx.addIssue({ code: "custom", path: ["url"], message: "url is required for source" });
124
161
  } else if (v.url !== void 0) {
125
162
  ctx.addIssue({ code: "custom", path: ["url"], message: "url is only allowed for source" });
126
163
  }
164
+ if (v.sourceSnapshot !== void 0 && v.kind !== "source") {
165
+ ctx.addIssue({ code: "custom", path: ["sourceSnapshot"], message: "sourceSnapshot is only allowed for source" });
166
+ }
167
+ if (v.confirmedRevision != null && (v.kind !== "source" || v.sourceSnapshot === void 0)) {
168
+ ctx.addIssue({ code: "custom", path: ["confirmedRevision"], message: "confirmedRevision requires a source snapshot" });
169
+ }
127
170
  if (v.kind === "diagram") {
128
171
  if (v.diagram === void 0)
129
172
  ctx.addIssue({ code: "custom", path: ["diagram"], message: "diagram is required for diagram" });
@@ -146,14 +189,17 @@ function refineKind(v, ctx) {
146
189
  ctx.addIssue({ code: "custom", path: ["priority"], message: "priority is only allowed for question" });
147
190
  }
148
191
  }
149
- var EntryFrontmatter = z2.object({
150
- id: z2.string().regex(ID_RE),
192
+ var EntryFrontmatter = z3.object({
193
+ id: z3.string().regex(ID_RE),
151
194
  kind: EntryKindSchema,
152
- title: z2.string().min(1).max(200),
195
+ title: z3.string().min(1).max(200),
153
196
  status: StatusSchema,
154
197
  businessDate: BusinessDate,
155
- related: z2.array(z2.string()),
156
- sourceMessages: z2.array(z2.string()),
198
+ related: z3.array(z3.string()),
199
+ sourceMessages: z3.array(z3.string()),
200
+ sourceSnapshot: SourceSnapshot.optional(),
201
+ sourceRefs: z3.array(SourceRef).max(10).default([]),
202
+ confirmedRevision: SourceRevision.default(null),
157
203
  url: HttpUrl.optional(),
158
204
  priority: Priority.optional(),
159
205
  diagram: DiagramMeta.optional(),
@@ -162,30 +208,31 @@ var EntryFrontmatter = z2.object({
162
208
  basis: BasisSchema.nullable(),
163
209
  ingestedAt: IsoDateTime,
164
210
  confirmedAt: IsoDateTime,
165
- quotes: z2.array(Quote)
211
+ quotes: z3.array(Quote)
166
212
  }).superRefine((v, ctx) => {
167
213
  refineKind(v, ctx);
168
214
  if (kindFromId(v.id) !== v.kind) {
169
215
  ctx.addIssue({ code: "custom", path: ["id"], message: "id prefix does not match kind" });
170
216
  }
171
217
  });
172
- var UpsertEntryInput = z2.object({
173
- id: z2.string().regex(ID_RE).optional(),
218
+ var UpsertEntryInput = z3.object({
219
+ id: z3.string().regex(ID_RE).optional(),
174
220
  kind: EntryKindSchema,
175
- title: z2.string().min(1).max(200),
176
- body: z2.string(),
221
+ title: z3.string().min(1).max(200),
222
+ body: z3.string(),
177
223
  status: StatusSchema.optional(),
178
224
  businessDate: BusinessDate.optional(),
179
- related: z2.array(z2.string()).optional(),
180
- sourceMessages: z2.array(z2.string()).default([]),
225
+ related: z3.array(z3.string()).optional(),
226
+ sourceMessages: z3.array(z3.string()).default([]),
227
+ sourceRefs: z3.array(SourceRef).max(10).optional(),
181
228
  url: HttpUrl.optional(),
182
229
  priority: Priority.optional(),
183
230
  /** Requirements only. The Room API checks it against reports; omitted keeps the stored one. */
184
231
  implementation: Implementation.optional(),
185
232
  basis: BasisSchema.optional(),
186
- quotes: z2.array(Quote).max(10).optional(),
187
- confirmed: z2.boolean().optional(),
188
- reason: z2.string().min(1).max(500)
233
+ quotes: z3.array(Quote).max(10).optional(),
234
+ confirmed: z3.boolean().optional(),
235
+ reason: z3.string().min(1).max(500)
189
236
  }).superRefine((v, ctx) => {
190
237
  refineKind(v, ctx);
191
238
  if (v.id !== void 0 && kindFromId(v.id) !== v.kind) {
@@ -194,23 +241,23 @@ var UpsertEntryInput = z2.object({
194
241
  });
195
242
 
196
243
  // ../../../packages/shared/dist/workspace.js
197
- import { z as z3 } from "zod";
198
- var MessageQuote = z3.object({
199
- messageId: z3.string(),
200
- role: z3.enum(["user", "agent", "system", "report"]),
201
- excerpt: z3.string()
244
+ import { z as z4 } from "zod";
245
+ var MessageQuote = z4.object({
246
+ messageId: z4.string(),
247
+ role: z4.enum(["user", "agent", "system", "report"]),
248
+ excerpt: z4.string()
202
249
  });
203
- var UserMessageMeta = z3.object({
250
+ var UserMessageMeta = z4.object({
204
251
  quote: MessageQuote.optional()
205
252
  });
206
- var AgentMessageMeta = z3.object({
207
- changes: z3.array(z3.object({
208
- entryId: z3.string(),
253
+ var AgentMessageMeta = z4.object({
254
+ changes: z4.array(z4.object({
255
+ entryId: z4.string(),
209
256
  kind: EntryKindSchema,
210
- title: z3.string(),
211
- op: z3.enum(["add", "update"])
257
+ title: z4.string(),
258
+ op: z4.enum(["add", "update"])
212
259
  })),
213
- askedQuestion: z3.object({ id: z3.string(), title: z3.string(), body: z3.string() }).nullable(),
260
+ askedQuestion: z4.object({ id: z4.string(), title: z4.string(), body: z4.string() }).nullable(),
214
261
  quote: MessageQuote.optional()
215
262
  });
216
263
  function isBranchName(name) {
@@ -626,49 +673,140 @@ function bundledExtensions() {
626
673
  ];
627
674
  }
628
675
 
676
+ // ../../../packages/shared/dist/extensionSource.js
677
+ var MAX_INPUT = 2048;
678
+ var OWNER2 = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
679
+ var REPO = /^[A-Za-z0-9._-]{1,100}$/;
680
+ var SEGMENT = /^[A-Za-z0-9._-]{1,255}$/;
681
+ var REF = /^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/;
682
+ function refuse(input) {
683
+ throw new Error(`unsupported extension source: ${input.length > 200 ? `${input.slice(0, 200)}\u2026` : input}`);
684
+ }
685
+ function plainSegment(segment) {
686
+ return SEGMENT.test(segment) && segment !== "." && segment !== "..";
687
+ }
688
+ function repoName(segment) {
689
+ const name = segment.endsWith(".git") ? segment.slice(0, -4) : segment;
690
+ return REPO.test(name) && name !== "." && name !== ".." ? name : null;
691
+ }
692
+ function parseSkillSource(raw) {
693
+ const input = raw.trim();
694
+ if (!input || input.length > MAX_INPUT)
695
+ refuse(input);
696
+ if (!input.includes(":") && !input.startsWith("github.com/") && /^[^/]+\/[^/]+$/.test(input)) {
697
+ const [owner2, repo2] = input.split("/");
698
+ const name = repoName(repo2);
699
+ if (!OWNER2.test(owner2) || !name)
700
+ refuse(input);
701
+ return { via: "github", owner: owner2, repo: name, ref: null, path: null, skill: null, refPath: null, exact: false };
702
+ }
703
+ let url;
704
+ try {
705
+ url = new URL(input.startsWith("github.com/") ? `https://${input}` : input);
706
+ } catch {
707
+ refuse(input);
708
+ }
709
+ if (url.protocol !== "https:" || url.username || url.password || url.port || url.search || url.hash)
710
+ refuse(input);
711
+ const typedPath = input.replace(/^https:\/\/[^/]+/, "").replace(/^github\.com/, "");
712
+ if (typedPath !== url.pathname)
713
+ refuse(input);
714
+ const segments = url.pathname.replace(/\/$/, "").split("/").slice(1);
715
+ if (segments.some((part) => part === ""))
716
+ refuse(input);
717
+ const host = url.hostname.toLowerCase();
718
+ if (host === "skills.sh" || host === "www.skills.sh") {
719
+ if (segments.length < 2 || segments.length > 3 || !segments.every(plainSegment))
720
+ refuse(input);
721
+ const name = repoName(segments[1]);
722
+ if (!OWNER2.test(segments[0]) || !name)
723
+ refuse(input);
724
+ return { via: "skills.sh", owner: segments[0], repo: name, ref: null, path: null, skill: segments[2] ?? null, refPath: null, exact: false };
725
+ }
726
+ if (host !== "github.com" && host !== "www.github.com")
727
+ refuse(input);
728
+ if (segments.length < 2)
729
+ refuse(input);
730
+ const [owner, repoSegment, kind, ref, ...rest] = segments;
731
+ const repo = repoName(repoSegment);
732
+ if (!OWNER2.test(owner) || !repo)
733
+ refuse(input);
734
+ if (kind === void 0)
735
+ return { via: "github", owner, repo, ref: null, path: null, skill: null, refPath: null, exact: false };
736
+ if (kind !== "tree" && kind !== "blob" || ref === void 0 || !REF.test(ref) || ref.includes(".."))
737
+ refuse(input);
738
+ if (!rest.every(plainSegment))
739
+ refuse(input);
740
+ let folder = rest;
741
+ if (kind === "blob") {
742
+ if (rest.at(-1) !== "SKILL.md")
743
+ refuse(input);
744
+ folder = rest.slice(0, -1);
745
+ }
746
+ return {
747
+ via: "github",
748
+ owner,
749
+ repo,
750
+ ref,
751
+ path: folder.length ? folder.join("/") : null,
752
+ skill: null,
753
+ refPath: [ref, ...folder].join("/"),
754
+ exact: kind === "blob"
755
+ };
756
+ }
757
+ var UNSAFE_NAME = /[\\/:*?"<>|\u0000-\u001f]/;
758
+ var WINDOWS_DEVICE = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(\..*)?$/i;
759
+ function safeFileName(name) {
760
+ return name !== "" && name !== "." && name !== ".." && name.length <= 255 && !UNSAFE_NAME.test(name) && !WINDOWS_DEVICE.test(name) && // Windows drops a trailing dot or space, so `a.` and `a` would be the same file there.
761
+ !/[. ]$/.test(name);
762
+ }
763
+ function safeRepoPath(relative) {
764
+ return relative.split("/").every(safeFileName);
765
+ }
766
+
629
767
  // ../../../packages/shared/dist/protocol.js
630
- import { z as z4 } from "zod";
631
- var DaemonToHubSchema = z4.union([
632
- z4.object({
633
- type: z4.literal("hello"),
634
- token: z4.string(),
635
- version: z4.string(),
636
- adapters: z4.array(z4.string()),
768
+ import { z as z5 } from "zod";
769
+ var DaemonToHubSchema = z5.union([
770
+ z5.object({
771
+ type: z5.literal("hello"),
772
+ token: z5.string(),
773
+ version: z5.string(),
774
+ adapters: z5.array(z5.string()),
637
775
  /** sha256 hex of ~/.nema/report-token, sent when that file exists. */
638
- reportTokenHash: z4.string().regex(/^[0-9a-f]{64}$/).optional()
776
+ reportTokenHash: z5.string().regex(/^[0-9a-f]{64}$/).optional()
639
777
  }),
640
- z4.object({
641
- type: z4.literal("task.event"),
642
- taskId: z4.string(),
643
- event: z4.object({ type: z4.enum(["progress", "agent_message", "tool_call"]), text: z4.string() })
778
+ z5.object({
779
+ type: z5.literal("task.event"),
780
+ taskId: z5.string(),
781
+ event: z5.object({ type: z5.enum(["progress", "agent_message", "tool_call"]), text: z5.string() })
644
782
  }),
645
- z4.object({ type: z4.literal("task.finished"), taskId: z4.string(), ok: z4.literal(true), reply: z4.string() }),
646
- z4.object({ type: z4.literal("task.finished"), taskId: z4.string(), ok: z4.literal(false), error: z4.string() }),
647
- z4.object({ type: z4.literal("rpc.response"), id: z4.string(), ok: z4.literal(true), result: z4.unknown() }),
648
- z4.object({ type: z4.literal("rpc.response"), id: z4.string(), ok: z4.literal(false), error: z4.string() }),
649
- z4.object({ type: z4.literal("repo.moved"), repoId: z4.string(), mainRef: z4.string(), from: z4.string().nullable(), to: z4.string() }),
650
- z4.object({ type: z4.literal("repo.fetched"), repoId: z4.string(), ok: z4.boolean(), error: z4.string().optional(), at: z4.string() })
783
+ z5.object({ type: z5.literal("task.finished"), taskId: z5.string(), ok: z5.literal(true), reply: z5.string() }),
784
+ z5.object({ type: z5.literal("task.finished"), taskId: z5.string(), ok: z5.literal(false), error: z5.string() }),
785
+ z5.object({ type: z5.literal("rpc.response"), id: z5.string(), ok: z5.literal(true), result: z5.unknown() }),
786
+ z5.object({ type: z5.literal("rpc.response"), id: z5.string(), ok: z5.literal(false), error: z5.string() }),
787
+ z5.object({ type: z5.literal("repo.moved"), repoId: z5.string(), mainRef: z5.string(), from: z5.string().nullable(), to: z5.string() }),
788
+ z5.object({ type: z5.literal("repo.fetched"), repoId: z5.string(), ok: z5.boolean(), error: z5.string().optional(), at: z5.string() })
651
789
  ]);
652
- var HubToDaemonSchema = z4.discriminatedUnion("type", [
790
+ var HubToDaemonSchema = z5.discriminatedUnion("type", [
653
791
  // workspaceIds: every workspace that still exists, so the daemon can remove the isolation dirs
654
792
  // (`~/.nema/ws/<id>`) of workspaces deleted while it was offline. Optional for older hubs.
655
- z4.object({ type: z4.literal("welcome"), workspaceIds: z4.array(z4.string()).optional() }),
793
+ z5.object({ type: z5.literal("welcome"), workspaceIds: z5.array(z5.string()).optional() }),
656
794
  // Sent to a connected daemon when a workspace is deleted, so it removes that workspace's isolation dir.
657
- z4.object({ type: z4.literal("workspace.deleted"), workspaceId: z4.string() }),
658
- z4.object({
659
- type: z4.literal("task.assign"),
660
- taskId: z4.string(),
661
- workspaceId: z4.string(),
662
- adapter: z4.string().min(1),
663
- confidential: z4.boolean(),
664
- prompt: z4.string(),
665
- runToken: z4.string(),
666
- hubUrl: z4.string(),
667
- pluginTools: z4.array(z4.object({ name: z4.string(), description: z4.string(), parameters: z4.record(z4.string(), z4.unknown()) })),
668
- limits: z4.object({ maxModelCalls: z4.number().int().min(1), maxToolCalls: z4.number().int().min(1), timeoutMs: z4.number().int().min(1) }).optional()
795
+ z5.object({ type: z5.literal("workspace.deleted"), workspaceId: z5.string() }),
796
+ z5.object({
797
+ type: z5.literal("task.assign"),
798
+ taskId: z5.string(),
799
+ workspaceId: z5.string(),
800
+ adapter: z5.string().min(1),
801
+ confidential: z5.boolean(),
802
+ prompt: z5.string(),
803
+ runToken: z5.string(),
804
+ hubUrl: z5.string(),
805
+ pluginTools: z5.array(z5.object({ name: z5.string(), description: z5.string(), parameters: z5.record(z5.string(), z5.unknown()) })),
806
+ limits: z5.object({ maxModelCalls: z5.number().int().min(1), maxToolCalls: z5.number().int().min(1), timeoutMs: z5.number().int().min(1) }).optional()
669
807
  }),
670
- z4.object({ type: z4.literal("rpc.request"), id: z4.string(), method: z4.string(), params: z4.unknown() }),
671
- z4.object({ type: z4.literal("error"), message: z4.string() })
808
+ z5.object({ type: z5.literal("rpc.request"), id: z5.string(), method: z5.string(), params: z5.unknown() }),
809
+ z5.object({ type: z5.literal("error"), message: z5.string() })
672
810
  ]);
673
811
  function parseHubToDaemon(raw) {
674
812
  return HubToDaemonSchema.parse(raw);
@@ -819,6 +957,8 @@ export {
819
957
  isBranchName,
820
958
  PLUGINS,
821
959
  bundledExtensions,
960
+ parseSkillSource,
961
+ safeRepoPath,
822
962
  parseHubToDaemon,
823
963
  parseGhCommand,
824
964
  GH_COMMAND_TOOLS
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseHubToDaemon
3
- } from "../chunks/chunk-HEIQINX3.js";
3
+ } from "../chunks/chunk-W4DUBATU.js";
4
4
 
5
5
  // daemon/connection.js
6
6
  import WebSocket from "ws";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  UpsertEntryInput
3
- } from "../chunks/chunk-HEIQINX3.js";
3
+ } from "../chunks/chunk-W4DUBATU.js";
4
4
 
5
5
  // direct/tools.js
6
6
  import { z } from "zod";
@@ -1,2 +1,17 @@
1
1
  import type { RpcRouter } from "../daemon/rpc.js";
2
- export declare function registerExtensionHandlers(router: RpcRouter, nemaHome: string): void;
2
+ import { type SkillCandidate, type SkillResolution, type SourceFetch } from "./source.js";
3
+ export type ExtensionRemoval = {
4
+ id: string;
5
+ removed: "managed" | "external";
6
+ };
7
+ /** A resolved source as the install preview shows it: where each skill would land, and whether it is already there. */
8
+ export type SourcePreview = SkillResolution & {
9
+ skills: Array<SkillCandidate & {
10
+ target: string | null;
11
+ installed: boolean;
12
+ }>;
13
+ };
14
+ export type ExtensionHandlerOptions = {
15
+ fetch?: SourceFetch;
16
+ };
17
+ export declare function registerExtensionHandlers(router: RpcRouter, nemaHome: string, options?: ExtensionHandlerOptions): void;
@@ -1,9 +1,84 @@
1
+ import {
2
+ parseSkillSource
3
+ } from "../chunks/chunk-W4DUBATU.js";
4
+
1
5
  // extensions/handlers.js
2
6
  import path from "node:path";
7
+ import { SkillInstaller } from "./install.js";
8
+ import { resolveSkillSource } from "./source.js";
3
9
  import { openExtensionStore } from "./store.js";
4
- function registerExtensionHandlers(router, nemaHome) {
5
- const store = openExtensionStore(path.join(nemaHome, "extensions"));
10
+ var RESOLVE_TIMEOUT_MS = 45e3;
11
+ function requireId(params, method) {
12
+ const id = params?.id;
13
+ if (typeof id !== "string" || !id)
14
+ throw new Error(`${method} needs a string id`);
15
+ return id;
16
+ }
17
+ function requireUseState(params) {
18
+ const useState = params?.useState;
19
+ if (useState !== "enabled" && useState !== "disabled")
20
+ throw new Error("extensions.setUse needs useState enabled or disabled");
21
+ return useState;
22
+ }
23
+ function registerExtensionHandlers(router, nemaHome, options = {}) {
24
+ const root = path.join(nemaHome, "extensions");
25
+ const store = openExtensionStore(root);
26
+ const fetch = options.fetch ?? globalThis.fetch;
27
+ const installer = new SkillInstaller({ root, store, fetch });
28
+ installer.recover();
6
29
  router.register("extensions.list", async () => store.list());
30
+ router.register("extensions.setUse", async (params) => store.setUseState(requireId(params, "extensions.setUse"), requireUseState(params)));
31
+ router.register("extensions.remove", async (params) => {
32
+ const id = requireId(params, "extensions.remove");
33
+ const record = store.list().find((row) => row.id === id);
34
+ if (!record)
35
+ throw new Error(`extension not found: ${id}`);
36
+ if (installer.running(id))
37
+ throw new Error(`${id} is installing; cancel the install and wait for it to stop first`);
38
+ if (record.provision === "bundled")
39
+ throw new Error("bundled extensions cannot be removed");
40
+ if (record.provision === "managed") {
41
+ store.removeManaged(id);
42
+ installer.forget(id);
43
+ return { id, removed: "managed" };
44
+ }
45
+ store.detach(id);
46
+ return { id, removed: "external" };
47
+ });
48
+ router.register("extensions.resolveSource", async (params) => {
49
+ const raw = params?.source;
50
+ if (typeof raw !== "string")
51
+ throw new Error("extensions.resolveSource needs a string source");
52
+ const source = parseSkillSource(raw);
53
+ let resolved;
54
+ try {
55
+ resolved = await resolveSkillSource(source, { fetch, signal: AbortSignal.timeout(RESOLVE_TIMEOUT_MS) });
56
+ } catch (error) {
57
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
58
+ throw new Error(`GitHub did not answer within ${RESOLVE_TIMEOUT_MS / 1e3} s`);
59
+ }
60
+ throw error;
61
+ }
62
+ const present = new Set(store.list().filter((row) => row.installState === "installed").map((row) => row.id));
63
+ return {
64
+ ...resolved,
65
+ skills: resolved.skills.map((skill) => ({
66
+ ...skill,
67
+ target: skill.id ? path.join(root, "extensions", skill.id) : null,
68
+ installed: skill.id ? present.has(skill.id) : false
69
+ }))
70
+ };
71
+ });
72
+ router.register("extensions.install", async (params) => {
73
+ const p = params ?? {};
74
+ if (typeof p.source !== "string" || typeof p.commit !== "string" || typeof p.path !== "string") {
75
+ throw new Error("extensions.install needs string source, commit and path");
76
+ }
77
+ return installer.start({ source: p.source, commit: p.commit, path: p.path });
78
+ });
79
+ router.register("extensions.installStatus", async (params) => installer.status(requireId(params, "extensions.installStatus")));
80
+ router.register("extensions.cancelInstall", async (params) => installer.cancel(requireId(params, "extensions.cancelInstall")));
81
+ router.register("extensions.retryInstall", async (params) => installer.retry(requireId(params, "extensions.retryInstall")));
7
82
  }
8
83
  export {
9
84
  registerExtensionHandlers
@@ -0,0 +1,70 @@
1
+ import { type SourceFetch, type TreeEntry } from "./source.js";
2
+ import type { openExtensionStore } from "./store.js";
3
+ type Store = ReturnType<typeof openExtensionStore>;
4
+ export type InstallState = "running" | "installed" | "failed" | "cancelled";
5
+ /** What the page polls while a skill installs, and what it reads afterwards. */
6
+ export type InstallJob = {
7
+ id: string;
8
+ state: InstallState;
9
+ /** Files written to staging so far, out of `total`. */
10
+ done: number;
11
+ total: number;
12
+ error: string | null;
13
+ commit: string | null;
14
+ path: string | null;
15
+ };
16
+ export type InstallRequest = {
17
+ source: string;
18
+ commit: string;
19
+ path: string;
20
+ };
21
+ export declare const INTERRUPTED = "interrupted: the daemon stopped before the install finished";
22
+ /**
23
+ * The files of one skill folder at one commit, checked before any byte is downloaded: regular files
24
+ * only, names safe on every platform, no two names that collide on a case-insensitive disk, and
25
+ * inside the count and size limits.
26
+ */
27
+ export declare function skillFiles(entries: TreeEntry[], folder: string): Array<TreeEntry & {
28
+ relative: string;
29
+ }>;
30
+ /**
31
+ * Installs one skill folder from a pinned commit into Nema's managed directory.
32
+ *
33
+ * The record goes in first as `installing`, carrying the commit and folder, so a crash leaves enough
34
+ * behind for `recover()` to mark it failed and for `retry()` to start again from the same commit.
35
+ * Files are downloaded into `<root>/staging/<id>-*`, each checked against its Git blob hash, and only
36
+ * a complete, verified folder is handed to `installManaged`, which swaps it in.
37
+ */
38
+ export declare class SkillInstaller {
39
+ private readonly o;
40
+ private readonly jobs;
41
+ /** Ids a retry has claimed while it re-reads the recorded commit, before its job exists. */
42
+ private readonly reserved;
43
+ private readonly staging;
44
+ constructor(o: {
45
+ root: string;
46
+ store: Store;
47
+ fetch: SourceFetch;
48
+ downloadTimeoutMs?: number;
49
+ });
50
+ /** Called once at daemon start: no install can be running yet, so every `installing` record is left over. */
51
+ recover(): string[];
52
+ /** True from the moment an install or retry is accepted until its cleanup has finished. */
53
+ running(id: string): boolean;
54
+ /** Called after a removal, so a finished job is not reported for an extension that is gone. */
55
+ forget(id: string): void;
56
+ start(request: InstallRequest): Promise<InstallJob>;
57
+ private begin;
58
+ /** Start again from the commit and folder on the record, for a failed, cancelled or interrupted install. */
59
+ retry(id: string): Promise<InstallJob>;
60
+ cancel(id: string): InstallJob;
61
+ status(id: string): InstallJob;
62
+ /** Resolves once the current install of `id` has stopped, whichever way. For tests and shutdown. */
63
+ settled(id: string): Promise<void>;
64
+ /** One install per id at a time; an installed copy is not replaced (updating is its own operation). */
65
+ private claim;
66
+ private snapshot;
67
+ private launch;
68
+ private run;
69
+ }
70
+ export {};