@asermax/pi-tentacles 1.0.0 → 1.2.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/README.md CHANGED
@@ -29,10 +29,10 @@ The helper reads these where it runs, on your machine. On pi, set them in pi's s
29
29
  | `TENTACLES_API_KEY` | the API key | required |
30
30
  | `TENTACLES_HELPER` | the helper's path | the bundled helper |
31
31
  | `TENTACLES_SESSION_PROVIDER` | the model provider | `cloudflare` |
32
- | `TENTACLES_SESSION_MODEL` | the model | `glm-5.2` |
32
+ | `TENTACLES_SESSION_MODEL` | the model | `glm-5.3` |
33
33
  | `TENTACLES_SESSION_EFFORT` | the effort level | `max` |
34
34
  | `TENTACLES_WORKSPACE_IMAGE` | the workspace image | `default` |
35
- | `TENTACLES_WORKSPACE_SIZE` | the workspace container's instance type | `basic` |
35
+ | `TENTACLES_WORKSPACE_SIZE` | the workspace container's instance type | `standard-2` |
36
36
  | `TENTACLES_WORKSPACE_LIFETIME` | the workspace's inactivity time, in minutes | `15` |
37
37
 
38
38
  A setting the agent passes to `spawn` wins over its variable.
@@ -15382,7 +15382,13 @@ var SessionStatuses = {
15382
15382
  var SessionStatus = Enum(SessionStatuses);
15383
15383
  var Question = Struct({
15384
15384
  id: QuestionId,
15385
- text: String$1
15385
+ text: String$1,
15386
+ repositoryCheck: Union([
15387
+ TaggedStruct("NoRepository", {}),
15388
+ TaggedStruct("Stopped", {}),
15389
+ TaggedStruct("Clean", {}),
15390
+ TaggedStruct("CheckFailed", { output: String$1 })
15391
+ ]).pipe(toTaggedUnion("_tag"))
15386
15392
  });
15387
15393
  var SessionDetails = Struct({
15388
15394
  settings: Settings,
@@ -17105,6 +17111,32 @@ var http = (options) => Object.assign(Object.create(Proto), {
17105
17111
  * @since 4.0.0
17106
17112
  */
17107
17113
  var bearer = /*#__PURE__*/ http({ scheme: "Bearer" });
17114
+ // ../../packages/worker-api/src/archive-kinds.ts
17115
+ /**
17116
+ * The kinds of archive a create request carries. `expandsInto` is where the workspace container
17117
+ * expands the kind's archive; a null one is read by the Worker only and never reaches the container.
17118
+ */
17119
+ var archiveKinds = {
17120
+ skills: {
17121
+ heading: "Skills",
17122
+ expandsInto: "/skills"
17123
+ },
17124
+ agents: {
17125
+ heading: "Agents",
17126
+ expandsInto: null
17127
+ },
17128
+ context: {
17129
+ heading: "Context",
17130
+ expandsInto: "/context"
17131
+ }
17132
+ };
17133
+ keys(archiveKinds).flatMap((kind) => {
17134
+ const { expandsInto } = archiveKinds[kind];
17135
+ return expandsInto == null ? [] : [{
17136
+ kind,
17137
+ directory: expandsInto
17138
+ }];
17139
+ });
17108
17140
  // ../../packages/worker-api/src/middleware.ts
17109
17141
  /** Every request carries the API key as a bearer token; a provider checks it against its secret. */
17110
17142
  var ApiKey = class extends Service()("tentacles/ApiKey", {
@@ -17121,8 +17153,7 @@ var CreateSession = Struct({
17121
17153
  parent: Parent,
17122
17154
  settings: Settings,
17123
17155
  prompt: String$1,
17124
- skills: optionalKey(Archive),
17125
- agents: optionalKey(Archive)
17156
+ ...map$5(archiveKinds, () => optionalKey(Archive))
17126
17157
  });
17127
17158
  var Prompt = Struct({
17128
17159
  text: String$1,
@@ -17209,10 +17240,15 @@ var endpointArguments = (name) => {
17209
17240
  };
17210
17241
  /** What the agent sends to spawn: the glue fills the parent. */
17211
17242
  var SpawnArguments = CreateSession.mapFields((fields) => ({
17212
- ...omit(fields, ["skills", "agents"]),
17243
+ ...omit(fields, [
17244
+ "skills",
17245
+ "agents",
17246
+ "context"
17247
+ ]),
17213
17248
  settings: optionalKey(Settings.mapFields(map(optionalKey))),
17214
17249
  skills: optionalKey(ArraySchema(String$1).annotate({ description: "Paths of skill folders on this machine, each holding a SKILL.md." })),
17215
- agents: optionalKey(ArraySchema(String$1).annotate({ description: "Paths of agent definition files on this machine." }))
17250
+ agents: optionalKey(ArraySchema(String$1).annotate({ description: "Paths of agent definition files on this machine." })),
17251
+ context: optionalKey(ArraySchema(String$1).annotate({ description: "Paths of files or folders on this machine; the remote agent finds them in /context, each under its own name." }))
17216
17252
  })).mapFields(omit(["parent"]));
17217
17253
  // src/core/tool-table.ts
17218
17254
  var ToolNames = {
@@ -17231,7 +17267,7 @@ var spawnDocument = toJsonSchemaDocument(SpawnArguments);
17231
17267
  var ToolTable = {
17232
17268
  spawn: ToolEntry.Endpoint({
17233
17269
  name: ToolNames.spawn,
17234
- description: "Start a remote agent with a name, a first prompt and optional settings. The settings left out come from the plugin's defaults. Optionally takes the paths of skill folders and agent definition files, which stay fixed for the session. Answers with the new session's id.",
17270
+ description: "Start a remote agent with a name, a first prompt and optional settings. The settings left out come from the plugin's defaults. Optionally takes the paths of skill folders and agent definition files, and the paths of context files or folders the remote agent finds in /context, which stay fixed for the session. Answers with the new session's id.",
17235
17271
  endpoint: "create",
17236
17272
  harnesses: bothHarnesses,
17237
17273
  arguments: {
@@ -17293,7 +17329,13 @@ var entryText = type().pipe(tagsExhaustive({
17293
17329
  toolResult: (entry) => entry.output,
17294
17330
  subagentResult: (entry) => entry.text
17295
17331
  }));
17296
- var questionWake = (session, question) => `${agentOf(session)} asks (question ${question.id}):\n${question.text}\n\nAnswer it with the answer tool, passing session ${session.sessionId} and question ${question.id}.`;
17332
+ var repositoryCheckText = type().pipe(tagsExhaustive({
17333
+ NoRepository: () => "NoRepository",
17334
+ Stopped: () => "Stopped",
17335
+ Clean: () => "Clean",
17336
+ CheckFailed: ({ output }) => output === "" ? "CheckFailed" : `CheckFailed\n${output}`
17337
+ }));
17338
+ var questionWake = (session, question) => `${agentOf(session)} asks (question ${question.id}):\n${question.text}\n\nRepository check: ${repositoryCheckText(question.repositoryCheck)}\n\nAnswer it with the answer tool, passing session ${session.sessionId} and question ${question.id}.`;
17297
17339
  var finishWake = (session, lastEntry) => lastEntry == null ? `${agentOf(session)} finished. Read its last message with the messages tool.` : `${agentOf(session)} finished. Its last message:\n${entryText(lastEntry)}`;
17298
17340
  var failureWake = (session, failure) => `${agentOf(session)} failed: ${failure}`;
17299
17341
  // src/core/controller.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asermax/pi-tentacles",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Spawn, steer, question and collect remote coding agents that run in tentacles.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@ A remote agent only keeps what it commits and pushes. Its workspace is disposabl
16
16
 
17
17
  ## Spawn
18
18
 
19
- `spawn` takes a name, the first prompt, the session's settings and, optionally, skills and agent definitions (see Skills and agents). The name shows in your status line, so name it after the agent's task. The settings you leave out come from the plugin's defaults.
19
+ `spawn` takes a name, the first prompt, the session's settings and, optionally, skills, agent definitions and context files (see Skills and agents, and Context files). The name shows in your status line, so name it after the agent's task. The settings you leave out come from the plugin's defaults.
20
20
 
21
21
  Write the first prompt for someone who has not seen this conversation: the goal, the repository, the branch to push, and what done looks like. Name the repository in the settings when the agent has to work in it, so tentacles clones the repository and, before the agent asks you anything, checks it for work that is not pushed.
22
22
 
@@ -30,6 +30,14 @@ Every definition is checked on your machine before anything is sent. When any fa
30
30
 
31
31
  A remote agent works on its own, so a skill that would stop to ask the person has nobody to ask. Write the handoff so the remote agent decides wherever a skill would stop for the user, ignores any instruction in a skill to ask the user, and lists every such decision in its final report. For a stop point it cannot decide, such as a credential it lacks, say in the handoff what to do, which can be asking you with its question tool.
32
32
 
33
+ ## Context files
34
+
35
+ `spawn` takes `context`, the paths of files and folders on your machine that the job needs and the repository does not hold, such as a change's scratch files. Each path lands in the remote agent's `/context` folder under its own name: a file at `/context/<file name>`, a folder at `/context/<folder name>/…`. `/context` is outside the repository, so the files never reach its commits. A folder takes everything under it, a symlink is sent as the file or folder it points to, and every file keeps its permission bits.
36
+
37
+ The remote agent has a copy that is fixed at spawn. Its edits to the copy are lost when its workspace restarts and never come back to you, so spawn a new remote agent when the job needs different files. The remote agent is told only that you sent files and that they are in `/context`, so say in the handoff what the files are and how to use them.
38
+
39
+ The paths are checked on your machine before anything is sent. A path that does not exist or cannot be read, two paths with the same name, or files that pack over 5 MB make `spawn` create nothing and return every problem with its path in a Context group, beside the Skills and Agents groups.
40
+
33
41
  ## Being woken
34
42
 
35
43
  You are woken when a remote agent stops. The wake carries the agent's last message when it finishes, the reason when it fails, and the question when it asks one. You never poll. If you think a wake was missed, run `list`.
@@ -38,6 +46,15 @@ You are woken when a remote agent stops. The wake carries the agent's last messa
38
46
 
39
47
  A remote agent asks with its question tool and waits for the answer. Answer with `answer`, passing the question's id. Answer it yourself when you can; when the decision is the human's, ask them and answer once they reply. A prompt you send while a question waits does not answer the question. The agent receives the prompt after the question is answered.
40
48
 
49
+ Every question comes with a repository check, which tells you whether the remote agent's work is known to be persisted:
50
+
51
+ - `Clean`: the work is committed and pushed. The question can wait any length of time.
52
+ - `Stopped`: the agent's workspace was not running, so it had nothing pending. The question can wait any length of time.
53
+ - `CheckFailed`: tentacles could not check the workspace, so the agent's work may exist only on the container's disk and is lost when the container stops. The check comes with what the git commands printed, which shows why it failed and is empty when the workspace ran neither command.
54
+ - `NoRepository`: the session's settings name no repository, so tentacles manages none and checked nothing.
55
+
56
+ A remote agent that has uncommitted changes or unpushed commits cannot ask: its question tool refuses until it commits and pushes.
57
+
41
58
  ## Prompt
42
59
 
43
60
  `prompt` sends a follow-up or a steer. A follow-up waits for the current run to end; a steer reaches the agent at its next step. Prompting a closed session reopens it.
package/tentacles-helper CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { readFile, readdir, stat } from "node:fs/promises";
4
- import { basename, join, relative, resolve, sep } from "node:path";
3
+ import * as fsp from "node:fs/promises";
4
+ import { readFile, realpath, stat } from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { createGzip } from "node:zlib";
9
+ import * as fs from "node:fs";
10
+ import { cpus } from "node:os";
11
+ import { Readable } from "node:stream";
5
12
  // \0rolldown/runtime.js
6
13
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
7
14
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
@@ -26240,7 +26247,13 @@ var SessionStatuses = {
26240
26247
  var SessionStatus = Enum(SessionStatuses);
26241
26248
  var Question = Struct({
26242
26249
  id: QuestionId,
26243
- text: String$2
26250
+ text: String$2,
26251
+ repositoryCheck: Union([
26252
+ TaggedStruct("NoRepository", {}),
26253
+ TaggedStruct("Stopped", {}),
26254
+ TaggedStruct("Clean", {}),
26255
+ TaggedStruct("CheckFailed", { output: String$2 })
26256
+ ]).pipe(toTaggedUnion("_tag"))
26244
26257
  });
26245
26258
  var SessionDetails = Struct({
26246
26259
  settings: Settings,
@@ -29256,6 +29269,34 @@ var http = (options) => Object.assign(Object.create(Proto$2), {
29256
29269
  * @since 4.0.0
29257
29270
  */
29258
29271
  var bearer = /*#__PURE__*/ http({ scheme: "Bearer" });
29272
+ // ../../packages/worker-api/src/archive-kinds.ts
29273
+ /**
29274
+ * The kinds of archive a create request carries. `expandsInto` is where the workspace container
29275
+ * expands the kind's archive; a null one is read by the Worker only and never reaches the container.
29276
+ */
29277
+ var archiveKinds = {
29278
+ skills: {
29279
+ heading: "Skills",
29280
+ expandsInto: "/skills"
29281
+ },
29282
+ agents: {
29283
+ heading: "Agents",
29284
+ expandsInto: null
29285
+ },
29286
+ context: {
29287
+ heading: "Context",
29288
+ expandsInto: "/context"
29289
+ }
29290
+ };
29291
+ /** The kinds in table order. */
29292
+ var archiveKindNames = keys(archiveKinds);
29293
+ archiveKindNames.flatMap((kind) => {
29294
+ const { expandsInto } = archiveKinds[kind];
29295
+ return expandsInto == null ? [] : [{
29296
+ kind,
29297
+ directory: expandsInto
29298
+ }];
29299
+ });
29259
29300
  // ../../packages/worker-api/src/middleware.ts
29260
29301
  /** Every request carries the API key as a bearer token; a provider checks it against its secret. */
29261
29302
  var ApiKey = class extends Service()("tentacles/ApiKey", {
@@ -29280,8 +29321,7 @@ var CreateSession = Struct({
29280
29321
  parent: Parent,
29281
29322
  settings: Settings,
29282
29323
  prompt: String$2,
29283
- skills: optionalKey(Archive),
29284
- agents: optionalKey(Archive)
29324
+ ...map$8(archiveKinds, () => optionalKey(Archive))
29285
29325
  });
29286
29326
  var Prompt = Struct({
29287
29327
  text: String$2,
@@ -30157,10 +30197,10 @@ var ConnectionConfig = all({
30157
30197
  /** The settings a spawn leaves out. */
30158
30198
  var SettingsDefaults = all({
30159
30199
  provider: String$1("TENTACLES_SESSION_PROVIDER").pipe(withDefault("cloudflare")),
30160
- model: String$1("TENTACLES_SESSION_MODEL").pipe(withDefault("glm-5.2")),
30200
+ model: String$1("TENTACLES_SESSION_MODEL").pipe(withDefault("glm-5.3")),
30161
30201
  effort: String$1("TENTACLES_SESSION_EFFORT").pipe(withDefault("max")),
30162
30202
  image: String$1("TENTACLES_WORKSPACE_IMAGE").pipe(withDefault("default")),
30163
- size: String$1("TENTACLES_WORKSPACE_SIZE").pipe(withDefault("basic")),
30203
+ size: String$1("TENTACLES_WORKSPACE_SIZE").pipe(withDefault("standard-2")),
30164
30204
  lifetime: Int("TENTACLES_WORKSPACE_LIFETIME").pipe(withDefault(15))
30165
30205
  });
30166
30206
  // src/helper/client.ts
@@ -30476,16 +30516,25 @@ var exhaustive = exhaustive$1;
30476
30516
  /**
30477
30517
  * What the helper receives for spawn: CreateSession with every setting
30478
30518
  * optional, since the helper fills the rest from its variables, and with the
30479
- * paths of the skills and agents in place of their archives, which the helper packs.
30519
+ * paths of the skills, agents and context in place of their archives, which the helper packs.
30480
30520
  */
30481
30521
  var SpawnRequest = CreateSession.mapFields((fields) => ({
30482
- ...omit(fields, ["skills", "agents"]),
30522
+ ...omit(fields, [
30523
+ "skills",
30524
+ "agents",
30525
+ "context"
30526
+ ]),
30483
30527
  settings: optionalKey(Settings.mapFields(map$3(optionalKey))),
30484
30528
  skills: optionalKey(ArraySchema(String$2).annotate({ description: "Paths of skill folders on this machine, each holding a SKILL.md." })),
30485
- agents: optionalKey(ArraySchema(String$2).annotate({ description: "Paths of agent definition files on this machine." }))
30529
+ agents: optionalKey(ArraySchema(String$2).annotate({ description: "Paths of agent definition files on this machine." })),
30530
+ context: optionalKey(ArraySchema(String$2).annotate({ description: "Paths of files or folders on this machine; the remote agent finds them in /context, each under its own name." }))
30486
30531
  }));
30487
30532
  SpawnRequest.mapFields(omit(["parent"]));
30488
30533
  // ../../node_modules/.pnpm/modern-tar@0.8.6/node_modules/modern-tar/dist/packer-By1HVV7Z.js
30534
+ var FILE = "file";
30535
+ var LINK = "link";
30536
+ var SYMLINK = "symlink";
30537
+ var DIRECTORY = "directory";
30489
30538
  var TYPEFLAG = {
30490
30539
  file: "0",
30491
30540
  link: "1",
@@ -30645,7 +30694,7 @@ function getHeaderBlocks(header) {
30645
30694
  ];
30646
30695
  }
30647
30696
  var EOF_BUFFER = /* @__PURE__ */ new Uint8Array(1024);
30648
- function createTarPacker$1(onData) {
30697
+ function createTarPacker(onData) {
30649
30698
  let currentHeader = null;
30650
30699
  let bytesWritten = 0;
30651
30700
  let finalized = false;
@@ -30694,171 +30743,6 @@ function createTarPacker$1(onData) {
30694
30743
  }
30695
30744
  };
30696
30745
  }
30697
- // ../../node_modules/.pnpm/modern-tar@0.8.6/node_modules/modern-tar/dist/web/index.js
30698
- function createTarPacker() {
30699
- let streamController;
30700
- let packer;
30701
- let bodyController = null;
30702
- let resume = null;
30703
- let drain = null;
30704
- let stopped = false;
30705
- let stopReason;
30706
- const unblock = () => {
30707
- resume?.();
30708
- resume = null;
30709
- drain = null;
30710
- };
30711
- const stop = (reason) => {
30712
- if (stopped) return;
30713
- stopped = true;
30714
- stopReason = reason;
30715
- bodyController?.error(reason);
30716
- bodyController = null;
30717
- streamController.error(reason);
30718
- unblock();
30719
- };
30720
- const abortBody = (reason = AbortSignal.abort().reason) => {
30721
- bodyController = null;
30722
- stop(reason);
30723
- };
30724
- const checkStopped = () => {
30725
- if (stopped) throw stopReason;
30726
- };
30727
- const emit = (operation) => {
30728
- checkStopped();
30729
- try {
30730
- operation();
30731
- } catch (error) {
30732
- stop(error);
30733
- throw error;
30734
- }
30735
- const pending = drain;
30736
- if (!pending) return;
30737
- const signal = bodyController?.signal;
30738
- if (!signal) return;
30739
- if (signal.aborted) abortBody(signal.reason);
30740
- else signal.onabort = () => abortBody(signal.reason);
30741
- return pending.then(checkStopped);
30742
- };
30743
- return {
30744
- readable: new ReadableStream({
30745
- start(controller) {
30746
- streamController = controller;
30747
- packer = createTarPacker$1((chunk) => {
30748
- const buffer = chunk.buffer;
30749
- controller.enqueue(buffer instanceof ArrayBuffer && !Reflect.get(buffer, "resizable") ? chunk : new Uint8Array(chunk));
30750
- if ((controller.desiredSize ?? 1) <= 0) drain ||= new Promise((resolve) => {
30751
- resume = resolve;
30752
- });
30753
- });
30754
- },
30755
- pull: unblock,
30756
- cancel(reason = AbortSignal.abort().reason) {
30757
- stop(reason);
30758
- }
30759
- }, new ByteLengthQueuingStrategy({ highWaterMark: 8388608 })),
30760
- controller: {
30761
- add(header) {
30762
- try {
30763
- checkStopped();
30764
- if (bodyController) throw new Error("Previous entry must be completed before adding a new one");
30765
- packer.add(header);
30766
- } catch (error) {
30767
- stop(error);
30768
- throw error;
30769
- }
30770
- return new WritableStream({
30771
- start(controller) {
30772
- bodyController = controller;
30773
- },
30774
- write(chunk) {
30775
- return emit(() => {
30776
- if (!ArrayBuffer.isView(chunk) || chunk[Symbol.toStringTag] !== "Uint8Array") throw new TypeError("Expected a Uint8Array body chunk");
30777
- packer.write(chunk);
30778
- });
30779
- },
30780
- close() {
30781
- const result = emit(() => packer.endEntry());
30782
- if (!result) {
30783
- bodyController = null;
30784
- return;
30785
- }
30786
- return result.then(() => {
30787
- bodyController = null;
30788
- });
30789
- },
30790
- abort: abortBody
30791
- });
30792
- },
30793
- finalize() {
30794
- try {
30795
- checkStopped();
30796
- if (bodyController) throw new Error("Cannot finalize while an entry is still active");
30797
- packer.finalize();
30798
- streamController.close();
30799
- } catch (error) {
30800
- stop(error);
30801
- throw error;
30802
- }
30803
- },
30804
- error(err) {
30805
- stop(err);
30806
- }
30807
- }
30808
- };
30809
- }
30810
- async function streamToBuffer(stream) {
30811
- const chunks = [];
30812
- const reader = stream.getReader();
30813
- let totalLength = 0;
30814
- try {
30815
- while (true) {
30816
- const { done, value } = await reader.read();
30817
- if (done) break;
30818
- chunks.push(value);
30819
- totalLength += value.length;
30820
- }
30821
- const result = new Uint8Array(totalLength);
30822
- let offset = 0;
30823
- for (const chunk of chunks) {
30824
- result.set(chunk, offset);
30825
- offset += chunk.length;
30826
- }
30827
- return result;
30828
- } finally {
30829
- reader.releaseLock();
30830
- }
30831
- }
30832
- async function packTar(entries) {
30833
- const { readable, controller } = createTarPacker();
30834
- const archive = streamToBuffer(readable);
30835
- try {
30836
- for (const entry of entries) {
30837
- const entryStream = controller.add(entry.header);
30838
- const body = "body" in entry ? entry.body : entry.data;
30839
- if (!body) {
30840
- await entryStream.close();
30841
- continue;
30842
- }
30843
- if (body instanceof ReadableStream) await body.pipeTo(entryStream);
30844
- else if (body instanceof Blob) await body.stream().pipeTo(entryStream);
30845
- else try {
30846
- const chunk = await normalizeBody(body);
30847
- if (chunk.length > 0) {
30848
- const writer = entryStream.getWriter();
30849
- await writer.write(chunk);
30850
- await writer.close();
30851
- } else await entryStream.close();
30852
- } catch {
30853
- throw new TypeError(`Unsupported content type for entry "${entry.header.name}".`);
30854
- }
30855
- }
30856
- controller.finalize();
30857
- } catch (error) {
30858
- controller.error(error);
30859
- }
30860
- return archive;
30861
- }
30862
30746
  // ../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js
30863
30747
  var require_identity = /* @__PURE__ */ __commonJSMin(((exports) => {
30864
30748
  var ALIAS = Symbol.for("yaml.alias");
@@ -37575,34 +37459,410 @@ var checkAgents = (sources) => {
37575
37459
  };
37576
37460
  TaggedError()("ArchiveUnreadable", { message: String$2 });
37577
37461
  var skillFile = "SKILL.md";
37578
- var gzip = async (bytes) => new Uint8Array(await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer());
37579
- /** Packs entries into a tar.gz, the form `readArchive` reads. */
37580
- var packArchive = (entries) => promise(async () => gzip(await packTar(entries.map((entry) => ({
37581
- header: {
37582
- name: entry.path,
37583
- size: entry.bytes.byteLength,
37584
- mode: entry.mode
37585
- },
37586
- body: entry.bytes
37587
- })))));
37588
37462
  new TextDecoder();
37463
+ // ../../node_modules/.pnpm/modern-tar@0.8.6/node_modules/modern-tar/dist/fs/index.js
37464
+ var win32Reserved = {
37465
+ ":": "",
37466
+ "<": "",
37467
+ ">": "",
37468
+ "|": "",
37469
+ "?": "",
37470
+ "*": "",
37471
+ "\"": ""
37472
+ };
37473
+ function normalizeName(name) {
37474
+ const path = name.replace(/\\/g, "/");
37475
+ if (path.split("/").includes("..") || /^[a-zA-Z]:\.\./.test(path)) throw new Error(`${name} points outside extraction directory`);
37476
+ let relative = path;
37477
+ if (/^[a-zA-Z]:/.test(relative)) relative = relative.replace(/^[a-zA-Z]:[/\\]?/, "");
37478
+ else if (relative.startsWith("/")) relative = relative.replace(/^\/+/, "");
37479
+ if (process.platform === "win32") return relative.replace(/[<>:"|?*]/g, (char) => win32Reserved[char]);
37480
+ return relative;
37481
+ }
37482
+ var BIGINT_STAT = { bigint: true };
37483
+ var WITH_FILE_TYPES = { withFileTypes: true };
37484
+ function packTar(sources, options = {}) {
37485
+ const results = /* @__PURE__ */ new Map();
37486
+ const fileHandles = /* @__PURE__ */ new Map();
37487
+ const bodyStreams = /* @__PURE__ */ new Set();
37488
+ let resume = null;
37489
+ let drain = null;
37490
+ let resumeWriter = null;
37491
+ let cancelError;
37492
+ const unblock = () => {
37493
+ const resolve = resume;
37494
+ resume = null;
37495
+ drain = null;
37496
+ resolve?.();
37497
+ };
37498
+ const wakeWriter = () => {
37499
+ resumeWriter?.();
37500
+ resumeWriter = null;
37501
+ };
37502
+ const destroyBody = (body, reason) => {
37503
+ bodyStreams.delete(body);
37504
+ body.destroy(reason);
37505
+ };
37506
+ const closeHandle = (handle) => {
37507
+ const closing = fileHandles.get(handle);
37508
+ if (closing !== null) return closing;
37509
+ const promise = handle.close().finally(() => fileHandles.delete(handle));
37510
+ fileHandles.set(handle, promise);
37511
+ return promise;
37512
+ };
37513
+ const stop = async (reason) => {
37514
+ for (const body of bodyStreams) destroyBody(body, reason);
37515
+ const closing = Promise.allSettled([...fileHandles.keys()].map(closeHandle));
37516
+ results.clear();
37517
+ wakeWriter();
37518
+ for (const result of await closing) if (result.status === "rejected") throw result.reason;
37519
+ };
37520
+ const stream = new Readable({
37521
+ highWaterMark: 8388608,
37522
+ read: unblock,
37523
+ destroy(error, callback) {
37524
+ cancelError = error ?? AbortSignal.abort().reason;
37525
+ unblock();
37526
+ stop(cancelError).then(() => callback(error), (closeError) => callback(error ?? closeError));
37527
+ }
37528
+ });
37529
+ const onError = (error) => stream.destroy(error);
37530
+ const packer = createTarPacker((chunk) => {
37531
+ if (stream.destroyed) throw cancelError;
37532
+ if (!stream.push(Buffer.from(chunk)) && !drain) drain = new Promise((resolve) => {
37533
+ resume = resolve;
37534
+ });
37535
+ });
37536
+ (async () => {
37537
+ const { dereference = false, filter, map, baseDir, concurrency = cpus().length || 8 } = options;
37538
+ let directoryPath;
37539
+ let realBaseDir;
37540
+ let jobs;
37541
+ if (typeof sources === "string") {
37542
+ const source = path.resolve(sources);
37543
+ directoryPath = source;
37544
+ const before = await fsp.stat(source, BIGINT_STAT);
37545
+ if (stream.destroyed) return;
37546
+ const entries = await fsp.readdir(source, WITH_FILE_TYPES);
37547
+ if (stream.destroyed) return;
37548
+ const after = await fsp.stat(source, BIGINT_STAT);
37549
+ if (stream.destroyed) return;
37550
+ jobs = before.dev === after.dev && before.ino === after.ino ? entries.map((entry) => ({
37551
+ type: entry.isDirectory() ? DIRECTORY : FILE,
37552
+ source: path.join(source, entry.name),
37553
+ target: entry.name
37554
+ })) : [];
37555
+ } else jobs = sources.map((source) => ({ ...source }));
37556
+ const seenHardlinks = /* @__PURE__ */ new Map();
37557
+ let jobIndex = 0;
37558
+ let writeIndex = 0;
37559
+ let activeWorkers = 0;
37560
+ let allJobsQueued = false;
37561
+ const writeStreamBody = async (body) => {
37562
+ try {
37563
+ for await (const chunk of body) {
37564
+ if (stream.destroyed) return;
37565
+ packer.write(chunk instanceof Uint8Array ? chunk : Buffer.from(chunk));
37566
+ if (drain) await drain;
37567
+ }
37568
+ } finally {
37569
+ body.off("error", onError);
37570
+ bodyStreams.delete(body);
37571
+ }
37572
+ };
37573
+ const writer = async () => {
37574
+ const readBufferSmall = Buffer.alloc(65536);
37575
+ let readBufferLarge = null;
37576
+ while (true) {
37577
+ if (stream.destroyed) return;
37578
+ if (allJobsQueued && writeIndex >= jobs.length) break;
37579
+ if (!results.has(writeIndex)) {
37580
+ await new Promise((resolve) => {
37581
+ resumeWriter = resolve;
37582
+ });
37583
+ continue;
37584
+ }
37585
+ const result = results.get(writeIndex);
37586
+ results.delete(writeIndex);
37587
+ if (!result) {
37588
+ writeIndex++;
37589
+ controller();
37590
+ continue;
37591
+ }
37592
+ if (result.hardlinkId) {
37593
+ const originalName = result.header.name;
37594
+ const hardlinkTarget = seenHardlinks.get(result.hardlinkId);
37595
+ if (hardlinkTarget) {
37596
+ if (result.body && !(result.body instanceof Uint8Array) && !(result.body instanceof Readable)) await closeHandle(result.body.handle);
37597
+ result.body = void 0;
37598
+ result.header.type = LINK;
37599
+ result.header.linkname = hardlinkTarget.originalName;
37600
+ result.header.size = 0;
37601
+ }
37602
+ if (map) result.header = map(result.header);
37603
+ if (hardlinkTarget) {
37604
+ if (result.header.linkname === hardlinkTarget.originalName) result.header.linkname = hardlinkTarget.mappedName;
37605
+ } else seenHardlinks.set(result.hardlinkId, {
37606
+ originalName,
37607
+ mappedName: result.header.name
37608
+ });
37609
+ }
37610
+ packer.add(result.header);
37611
+ if (drain) await drain;
37612
+ if (stream.destroyed) return;
37613
+ if (result.body) if (result.body instanceof Uint8Array) {
37614
+ if (result.body.length > 0) {
37615
+ packer.write(result.body);
37616
+ if (drain) await drain;
37617
+ }
37618
+ } else if (result.body instanceof Readable) await writeStreamBody(result.body);
37619
+ else {
37620
+ const { handle, size } = result.body;
37621
+ const readBuffer = size > 1048576 ? readBufferLarge ??= Buffer.alloc(1048576) : readBufferSmall;
37622
+ try {
37623
+ let bytesLeft = size;
37624
+ while (bytesLeft > 0 && !stream.destroyed) {
37625
+ const { bytesRead } = await handle.read(readBuffer, 0, Math.min(bytesLeft, readBuffer.length), null);
37626
+ if (bytesRead === 0) break;
37627
+ packer.write(readBuffer.subarray(0, bytesRead));
37628
+ bytesLeft -= bytesRead;
37629
+ if (drain) await drain;
37630
+ }
37631
+ } finally {
37632
+ await closeHandle(handle);
37633
+ }
37634
+ }
37635
+ if (stream.destroyed) return;
37636
+ packer.endEntry();
37637
+ if (drain) await drain;
37638
+ writeIndex++;
37639
+ controller();
37640
+ }
37641
+ };
37642
+ const controller = () => {
37643
+ if (stream.destroyed || allJobsQueued) return;
37644
+ while (activeWorkers < concurrency && jobIndex < jobs.length && jobIndex - writeIndex < concurrency) {
37645
+ activeWorkers++;
37646
+ const currentIndex = jobIndex++;
37647
+ processJob(jobs[currentIndex], currentIndex).catch(onError).finally(() => {
37648
+ activeWorkers--;
37649
+ controller();
37650
+ });
37651
+ }
37652
+ if (activeWorkers === 0 && jobIndex >= jobs.length) {
37653
+ allJobsQueued = true;
37654
+ wakeWriter();
37655
+ }
37656
+ };
37657
+ const processJob = async (job, index) => {
37658
+ let jobResult = null;
37659
+ const target = normalizeName(job.target);
37660
+ try {
37661
+ if (job.type === "content" || job.type === "stream") {
37662
+ let body;
37663
+ let size;
37664
+ const isDir = target.endsWith("/");
37665
+ if (job.type === "stream") {
37666
+ if (!isDir && job.size <= 0 || isDir && job.size !== 0) throw new Error(isDir ? "Streams for directories must have size 0." : "Streams require a positive size.");
37667
+ size = job.size;
37668
+ } else {
37669
+ const content = await normalizeBody(job.content);
37670
+ size = content.length;
37671
+ body = content;
37672
+ }
37673
+ const stat = {
37674
+ size: isDir ? 0 : size,
37675
+ isFile: () => !isDir,
37676
+ isDirectory: () => isDir,
37677
+ isSymbolicLink: () => false,
37678
+ mode: job.mode,
37679
+ mtime: job.mtime ?? /* @__PURE__ */ new Date(),
37680
+ uid: job.uid ?? 0,
37681
+ gid: job.gid ?? 0
37682
+ };
37683
+ if (stream.destroyed) return;
37684
+ if (filter && !filter(target, stat)) return;
37685
+ if (stream.destroyed) return;
37686
+ let header = {
37687
+ name: target,
37688
+ type: isDir ? DIRECTORY : FILE,
37689
+ size: isDir ? 0 : size,
37690
+ mode: stat.mode,
37691
+ mtime: stat.mtime,
37692
+ uid: stat.uid,
37693
+ gid: stat.gid,
37694
+ uname: job.uname,
37695
+ gname: job.gname
37696
+ };
37697
+ if (map) header = map(header);
37698
+ if (stream.destroyed) return;
37699
+ if (!isDir && job.type === "stream") {
37700
+ body = job.content instanceof Readable ? job.content : Readable.fromWeb(job.content);
37701
+ body.once("error", onError);
37702
+ bodyStreams.add(body);
37703
+ }
37704
+ jobResult = {
37705
+ header,
37706
+ body: isDir ? void 0 : body
37707
+ };
37708
+ return;
37709
+ }
37710
+ let source = job.source;
37711
+ let stat = await fsp.lstat(source, BIGINT_STAT);
37712
+ if (stream.destroyed) return;
37713
+ if (dereference && (baseDir !== void 0 || stat.isSymbolicLink())) {
37714
+ const resolvedSource = await fsp.realpath(source);
37715
+ if (stream.destroyed) return;
37716
+ realBaseDir ??= await fsp.realpath(baseDir ?? directoryPath ?? process.cwd());
37717
+ if (stream.destroyed) return;
37718
+ const relativeToBase = path.relative(realBaseDir, resolvedSource);
37719
+ if (relativeToBase === ".." || relativeToBase.startsWith(".." + path.sep) || path.isAbsolute(relativeToBase)) return;
37720
+ if (stat.isSymbolicLink()) {
37721
+ source = resolvedSource;
37722
+ stat = await fsp.lstat(source, BIGINT_STAT);
37723
+ if (stat.isSymbolicLink()) return;
37724
+ }
37725
+ }
37726
+ if (stream.destroyed) return;
37727
+ if (filter && !filter(job.source, stat)) return;
37728
+ if (stream.destroyed) return;
37729
+ let header = {
37730
+ name: target,
37731
+ size: 0,
37732
+ mode: (job.mode ?? Number(stat.mode)) & 4095,
37733
+ mtime: job.mtime === void 0 ? stat.mtime : new Date(job.mtime.getTime()),
37734
+ uid: job.uid ?? Number(stat.uid),
37735
+ gid: job.gid ?? Number(stat.gid),
37736
+ uname: job.uname,
37737
+ gname: job.gname,
37738
+ type: FILE
37739
+ };
37740
+ let body;
37741
+ let hardlinkId;
37742
+ if (stat.isDirectory()) {
37743
+ header.type = DIRECTORY;
37744
+ header.name = target.endsWith("/") ? target : `${target}/`;
37745
+ try {
37746
+ const entries = await fsp.readdir(source, WITH_FILE_TYPES);
37747
+ if (stream.destroyed) return;
37748
+ const after = await fsp.lstat(source, BIGINT_STAT);
37749
+ if (stream.destroyed || !after.isDirectory() || stat.dev !== after.dev || stat.ino !== after.ino) return;
37750
+ for (const d of entries) jobs.push({
37751
+ type: d.isDirectory() ? DIRECTORY : FILE,
37752
+ source: path.join(source, d.name),
37753
+ target: `${header.name}${d.name}`,
37754
+ mtime: job.mtime,
37755
+ uid: job.uid,
37756
+ gid: job.gid,
37757
+ uname: job.uname,
37758
+ gname: job.gname,
37759
+ mode: job.mode
37760
+ });
37761
+ } catch (error) {
37762
+ const code = error.code;
37763
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
37764
+ }
37765
+ } else if (stat.isSymbolicLink()) {
37766
+ header.type = SYMLINK;
37767
+ header.linkname = await fsp.readlink(job.source);
37768
+ } else if (stat.isFile()) {
37769
+ header.size = Number(stat.size);
37770
+ let handleToClose;
37771
+ if (stat.nlink > 1n && (process.platform !== "win32" || stat.dev !== 0n && stat.ino !== -1n)) hardlinkId = `${stat.dev}:${stat.ino}`;
37772
+ try {
37773
+ let after;
37774
+ try {
37775
+ if (header.size === 0) after = await fsp.lstat(source, BIGINT_STAT);
37776
+ else {
37777
+ handleToClose = await fsp.open(source, fs.constants.O_NOFOLLOW ?? 0);
37778
+ fileHandles.set(handleToClose, null);
37779
+ }
37780
+ } catch (error) {
37781
+ const code = error.code;
37782
+ if (code === "ELOOP" || code === "ENOENT") return;
37783
+ throw error;
37784
+ }
37785
+ if (stream.destroyed) return;
37786
+ if (after) {
37787
+ if (!after.isFile() || stat.dev !== after.dev || stat.ino !== after.ino) return;
37788
+ } else {
37789
+ const { dev, ino } = await handleToClose.stat(BIGINT_STAT);
37790
+ if (stream.destroyed) return;
37791
+ if (stat.dev !== dev || stat.ino !== ino) return;
37792
+ }
37793
+ if (header.size > 0) {
37794
+ const handle = handleToClose;
37795
+ if (header.size < 32768) {
37796
+ const buffer = Buffer.allocUnsafe(header.size);
37797
+ let offset = 0;
37798
+ while (offset < buffer.length && !stream.destroyed) {
37799
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
37800
+ if (bytesRead === 0) break;
37801
+ offset += bytesRead;
37802
+ }
37803
+ body = offset === buffer.length ? buffer : buffer.subarray(0, offset);
37804
+ } else {
37805
+ body = {
37806
+ handle,
37807
+ size: header.size
37808
+ };
37809
+ handleToClose = void 0;
37810
+ }
37811
+ }
37812
+ } finally {
37813
+ if (handleToClose) await closeHandle(handleToClose);
37814
+ }
37815
+ } else return;
37816
+ if (stream.destroyed) return;
37817
+ if (hardlinkId) jobResult = {
37818
+ header,
37819
+ body,
37820
+ hardlinkId
37821
+ };
37822
+ else {
37823
+ if (map) header = map(header);
37824
+ jobResult = {
37825
+ header,
37826
+ body
37827
+ };
37828
+ }
37829
+ } finally {
37830
+ if (stream.destroyed) {
37831
+ if (jobResult?.body instanceof Readable) destroyBody(jobResult.body, cancelError);
37832
+ else if (jobResult?.body && !(jobResult.body instanceof Uint8Array)) await closeHandle(jobResult.body.handle);
37833
+ } else {
37834
+ results.set(index, jobResult);
37835
+ if (index === writeIndex) wakeWriter();
37836
+ }
37837
+ }
37838
+ };
37839
+ controller();
37840
+ await writer();
37841
+ if (!stream.destroyed) {
37842
+ packer.finalize();
37843
+ stream.push(null);
37844
+ }
37845
+ })().catch(onError);
37846
+ return stream;
37847
+ }
37848
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | (fs.constants.O_NOFOLLOW ?? 0) | fs.constants.O_EXCL;
37849
+ Promise.resolve();
37850
+ process.platform;
37589
37851
  // src/helper/definitions.ts
37590
- /** Every problem of every path, per group; nothing is sent. */
37591
- var Rejected = TaggedStruct("Rejected", {
37592
- skills: ArraySchema(Problem),
37593
- agents: ArraySchema(Problem)
37594
- });
37852
+ /** Every problem of every path, per kind; nothing is sent. */
37853
+ var Rejected = TaggedStruct("Rejected", { ...map$8(archiveKinds, () => ArraySchema(Problem)) });
37595
37854
  var decoder = new TextDecoder();
37596
37855
  var encoder = new TextEncoder();
37597
- /** A definition file as the helper sends it: its text with the frontmatter repaired when strict YAML rejects it, and the entry carrying that text. */
37598
- var definitionFile = (path, bytes, mode) => {
37856
+ /** A definition file as the helper sends it: its text with the frontmatter repaired when strict YAML rejects it, and the package source carrying that text. */
37857
+ var definitionFile = (target, bytes, mode) => {
37599
37858
  const original = decoder.decode(bytes);
37600
37859
  const text = repairFrontmatter(original);
37601
37860
  return {
37602
37861
  text,
37603
- entry: {
37604
- path,
37605
- bytes: text === original ? bytes : encoder.encode(text),
37862
+ source: {
37863
+ type: "content",
37864
+ target,
37865
+ content: text === original ? bytes : encoder.encode(text),
37606
37866
  mode
37607
37867
  }
37608
37868
  };
@@ -37613,87 +37873,136 @@ var problemOf = (path, message) => ({
37613
37873
  message
37614
37874
  });
37615
37875
  var isNotFound = (cause) => hasProperty(cause, "code") && cause.code === "ENOENT";
37616
- var inspect = (path, kind) => tryPromise({
37876
+ var statPath = (path) => tryPromise({
37617
37877
  try: () => stat(path),
37618
37878
  catch: (cause) => problemOf(path, isNotFound(cause) ? "does not exist" : `cannot be read: ${String(cause)}`)
37619
- }).pipe(filterOrFail((info) => kind === "folder" ? info.isDirectory() : info.isFile(), () => problemOf(path, `is not a ${kind}`)));
37620
- var readFolder = (path, folder) => tryPromise({
37621
- try: async () => {
37622
- const dirents = await readdir(path, {
37623
- recursive: true,
37624
- withFileTypes: true
37625
- });
37626
- return (await Promise.all(dirents.map(async (dirent) => {
37627
- const file = join(dirent.parentPath, dirent.name);
37628
- const info = await stat(file);
37629
- if (!info.isFile()) return;
37630
- return {
37631
- path: `${folder}/${relative(path, file).split(sep).join("/")}`,
37632
- bytes: await readFile(file),
37633
- mode: info.mode & 511
37634
- };
37635
- }))).filter((entry) => entry != null).toSorted((left, right) => left.path.localeCompare(right.path));
37636
- },
37637
- catch: (cause) => problemOf(path, `cannot be read: ${String(cause)}`)
37879
+ });
37880
+ var inspect = (path, kind) => statPath(path).pipe(filterOrFail((info) => kind === "folder" ? info.isDirectory() : info.isFile(), () => problemOf(path, `is not a ${kind}`)));
37881
+ var readText = (path, problemPath) => tryPromise({
37882
+ try: () => readFile(path),
37883
+ catch: (cause) => problemOf(problemPath, `cannot be read: ${String(cause)}`)
37638
37884
  });
37639
37885
  var readSkill = (path) => gen(function* () {
37640
37886
  yield* inspect(path, "folder");
37641
- const folder = basename(resolve(path));
37642
- const entries = yield* readFolder(path, folder);
37643
- const skill = entries.find((entry) => entry.path === `${folder}/${skillFile}`);
37644
- if (skill == null) return yield* fail$3(problemOf(path, `has no ${skillFile}`));
37645
- const { text, entry } = definitionFile(skill.path, skill.bytes, skill.mode);
37887
+ const root = resolve(path);
37888
+ const folder = basename(root);
37889
+ const skillPath = join(root, skillFile);
37890
+ const missing = problemOf(path, `has no ${skillFile}`);
37891
+ const info = yield* tryPromise({
37892
+ try: () => stat(skillPath),
37893
+ catch: (cause) => isNotFound(cause) ? missing : problemOf(path, `cannot be read: ${String(cause)}`)
37894
+ }).pipe(filterOrFail((found) => found.isFile(), () => missing));
37895
+ const { text, source } = definitionFile(`${folder}/${skillFile}`, yield* readText(skillPath, path), info.mode & 511);
37896
+ const realRoot = yield* tryPromise({
37897
+ try: () => realpath(root),
37898
+ catch: (cause) => problemOf(path, `cannot be read: ${String(cause)}`)
37899
+ });
37646
37900
  return {
37647
- source: {
37901
+ skill: {
37648
37902
  folder,
37649
37903
  path,
37650
37904
  text
37651
37905
  },
37652
- entries: entries.map((existing) => existing === skill ? entry : existing)
37906
+ sources: [{
37907
+ type: "directory",
37908
+ source: realRoot,
37909
+ target: folder
37910
+ }, source],
37911
+ replaced: join(realRoot, skillFile)
37653
37912
  };
37654
37913
  });
37655
37914
  var readAgent = (path) => gen(function* () {
37656
37915
  const info = yield* inspect(path, "file");
37657
- const bytes = yield* tryPromise({
37658
- try: () => readFile(path),
37659
- catch: (cause) => problemOf(path, `cannot be read: ${String(cause)}`)
37660
- });
37661
- const { text, entry } = definitionFile(basename(path), bytes, info.mode & 511);
37916
+ const { text, source } = definitionFile(basename(path), yield* readText(path, path), info.mode & 511);
37662
37917
  return {
37663
- source: {
37918
+ agent: {
37664
37919
  path,
37665
37920
  text
37666
37921
  },
37667
- entries: [entry]
37922
+ sources: [source]
37668
37923
  };
37669
37924
  });
37670
- var packGroup = (kind, problems, entries) => gen(function* () {
37671
- if (problems.length > 0) return yield* fail$3(problems);
37672
- if (entries.length === 0) return;
37673
- const archive = yield* packArchive(entries);
37674
- if (archive.byteLength > 5242880) return yield* fail$3([problemOf(`the ${kind} archive`, `is ${megabytes(archive.byteLength)}, over the ${megabytes(maxArchiveBytes)} limit`)]);
37675
- return archive;
37925
+ /** A context path is checked by its stat alone: nothing in a folder is read here. */
37926
+ var readContext = (path) => gen(function* () {
37927
+ const info = yield* statPath(path);
37928
+ if (!info.isDirectory() && !info.isFile()) return yield* fail$3(problemOf(path, "is not a file or a folder"));
37929
+ const source = resolve(path);
37930
+ const target = basename(source);
37931
+ return info.isDirectory() ? {
37932
+ type: "directory",
37933
+ source,
37934
+ target
37935
+ } : {
37936
+ type: "file",
37937
+ source,
37938
+ target
37939
+ };
37940
+ });
37941
+ /** A problem on every context path whose name an earlier path already has. */
37942
+ var duplicateNames = (paths) => {
37943
+ const firstByName = /* @__PURE__ */ new Map();
37944
+ return paths.flatMap((path) => {
37945
+ const name = basename(resolve(path));
37946
+ const first = firstByName.get(name);
37947
+ if (first == null) {
37948
+ firstByName.set(name, path);
37949
+ return [];
37950
+ }
37951
+ return [problemOf(path, `has the same name as ${first}`)];
37952
+ });
37953
+ };
37954
+ /** Checks every path with the kind's reader (a stat for context paths, a read of the definition file for skills and agents), adds the problems the kind finds across the readable ones, and builds the result only when there is none. */
37955
+ var checkPaths = (paths, read, problemsOf, build) => gen(function* () {
37956
+ const [reads, unreadable] = yield* partition(paths, read, { concurrency: "unbounded" });
37957
+ const problems = [...unreadable, ...problemsOf(reads)];
37958
+ return problems.length > 0 ? yield* fail$3(problems) : build(reads);
37959
+ });
37960
+ var checkSkillPaths = (paths) => checkPaths(paths, readSkill, (reads) => checkSkills(reads.map((read) => read.skill)).problems, (reads) => {
37961
+ const replaced = new Set(reads.map((read) => read.replaced));
37962
+ return {
37963
+ sources: reads.flatMap((read) => read.sources),
37964
+ filter: (path) => !replaced.has(path)
37965
+ };
37966
+ });
37967
+ var checkAgentPaths = (paths) => checkPaths(paths, readAgent, (reads) => checkAgents(reads.map((read) => read.agent)).problems, (reads) => ({ sources: reads.flatMap((read) => read.sources) }));
37968
+ var checkContextPaths = (paths) => checkPaths(paths, readContext, () => duplicateNames(paths), (sources) => ({ sources }));
37969
+ var checks = {
37970
+ skills: checkSkillPaths,
37971
+ agents: checkAgentPaths,
37972
+ context: checkContextPaths
37973
+ };
37974
+ var pack = (kind, { sources, ...options }) => tryPromise({
37975
+ try: async () => {
37976
+ const chunks = [];
37977
+ await pipeline(packTar(sources, {
37978
+ dereference: true,
37979
+ baseDir: "/",
37980
+ ...options
37981
+ }), createGzip(), async (packed) => {
37982
+ for await (const chunk of packed) chunks.push(chunk);
37983
+ });
37984
+ return Buffer.concat(chunks);
37985
+ },
37986
+ catch: (cause) => [problemOf(`the ${kind} archive`, `cannot be packed: ${String(cause)}`)]
37987
+ }).pipe(filterOrFail((archive) => archive.byteLength <= maxArchiveBytes, (archive) => [problemOf(`the ${kind} archive`, `is ${megabytes(archive.byteLength)}, over the ${megabytes(maxArchiveBytes)} limit`)]));
37988
+ var archiveOf = (kind, paths) => gen(function* () {
37989
+ if (paths.length === 0) return;
37990
+ return yield* checks[kind](paths).pipe(flatMap((checked) => pack(kind, checked)));
37676
37991
  }).pipe(result$1);
37677
- /** Reads every path the agent passed, runs worker-api's checks on each group and packs the archives, each present only when its group had paths; fails with every problem of every path, never at the first. */
37678
- var readDefinitions = ({ skills: skillPaths, agents: agentPaths }) => gen(function* () {
37679
- const [[skillReads, skillUnreadable], [agentReads, agentUnreadable]] = yield* all$1([partition(skillPaths, readSkill, { concurrency: "unbounded" }), partition(agentPaths, readAgent, { concurrency: "unbounded" })], { concurrency: 2 });
37680
- const [skills, agents] = yield* all$1([packGroup("skills", [...skillUnreadable, ...checkSkills(skillReads.map((read) => read.source)).problems], skillReads.flatMap((read) => read.entries)), packGroup("agents", [...agentUnreadable, ...checkAgents(agentReads.map((read) => read.source)).problems], agentReads.flatMap((read) => read.entries))], { concurrency: 2 });
37681
- if (isFailure(skills) || isFailure(agents)) return yield* fail$3(Rejected.make({
37682
- skills: isFailure(skills) ? skills.failure : [],
37683
- agents: isFailure(agents) ? agents.failure : []
37684
- }));
37992
+ /** Checks every path the agent passed, kind by kind, and packs each kind that had paths into an archive; fails with every problem of every path, never at the first. */
37993
+ var packSpawnArchives = (paths) => gen(function* () {
37994
+ const results = yield* all$1(map$8(paths, (kindPaths, kind) => archiveOf(kind, kindPaths)), { concurrency: "unbounded" });
37995
+ if (Object.values(results).some(isFailure)) return yield* fail$3(Rejected.make(map$8(results, (result) => isFailure(result) ? result.failure : [])));
37685
37996
  const archives = {};
37686
- if (skills.success != null) archives.skills = skills.success;
37687
- if (agents.success != null) archives.agents = agents.success;
37997
+ for (const kind of archiveKindNames) {
37998
+ const result = results[kind];
37999
+ if (isSuccess$1(result) && result.success != null) archives[kind] = result.success;
38000
+ }
37688
38001
  return archives;
37689
38002
  });
37690
38003
  var section = (heading, problems) => `${heading}:\n${problems.map((problem) => `- ${problem.path}: ${problem.message}`).join("\n")}`;
37691
- /** The tool's error text: every problem with its path, the skills' under one heading and the agents' under another. */
37692
- var rejectionText = (rejected) => [
37693
- "No session was created: some skills or agents have problems.",
37694
- ...rejected.skills.length > 0 ? [section("Skills", rejected.skills)] : [],
37695
- ...rejected.agents.length > 0 ? [section("Agents", rejected.agents)] : []
37696
- ].join("\n\n");
38004
+ /** The tool's error text: every problem with its path, one section per kind with problems, in the kinds table's order. */
38005
+ var rejectionText = (rejected) => ["No session was created: some skills, agents or context have problems.", ...archiveKindNames.flatMap((kind) => rejected[kind].length > 0 ? [section(archiveKinds[kind].heading, rejected[kind])] : [])].join("\n\n");
37697
38006
  // src/helper/command.ts
37698
38007
  var sessionFields = { sessionId: SessionId };
37699
38008
  var PromptRequest = Struct({
@@ -37721,9 +38030,10 @@ var commands = {
37721
38030
  const request = yield* decodeArguments(SpawnRequest, json);
37722
38031
  const defaults = yield* SettingsDefaults;
37723
38032
  const client = yield* TentaclesClient;
37724
- const archives = yield* readDefinitions({
38033
+ const archives = yield* packSpawnArchives({
37725
38034
  skills: request.skills ?? [],
37726
- agents: request.agents ?? []
38035
+ agents: request.agents ?? [],
38036
+ context: request.context ?? []
37727
38037
  });
37728
38038
  return yield* client.sessions.create({ payload: {
37729
38039
  name: request.name,