@mclawnet/swarm 0.1.5 → 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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=project-files.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/project-files.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync, existsSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { PROJECT_FILE_WHITELIST, ProjectFilesError, isWhitelistedRelPath, listProjectFiles, readProjectFile, writeProjectFile, } from "../project-files.js";
6
+ let workDir;
7
+ beforeEach(() => {
8
+ workDir = mkdtempSync(join(tmpdir(), "clawnet-project-files-"));
9
+ });
10
+ afterEach(() => {
11
+ rmSync(workDir, { recursive: true, force: true });
12
+ });
13
+ describe("isWhitelistedRelPath", () => {
14
+ it("accepts the four canonical paths", () => {
15
+ for (const p of PROJECT_FILE_WHITELIST)
16
+ expect(isWhitelistedRelPath(p)).toBe(true);
17
+ });
18
+ it("rejects everything else", () => {
19
+ for (const p of [
20
+ "claude.md", // wrong case
21
+ "../CLAUDE.md", // traversal
22
+ ".claude/rules/x.md", // not in P1 whitelist
23
+ ".git/config",
24
+ "",
25
+ "CLAUDE.md/", // trailing slash mismatch
26
+ ])
27
+ expect(isWhitelistedRelPath(p)).toBe(false);
28
+ });
29
+ });
30
+ describe("listProjectFiles", () => {
31
+ it("returns whitelist entries with exists=false when nothing on disk", () => {
32
+ const files = listProjectFiles(workDir);
33
+ expect(files).toHaveLength(PROJECT_FILE_WHITELIST.length);
34
+ for (const f of files) {
35
+ expect(f.exists).toBe(false);
36
+ expect(f.loadedByCli).toBe(true);
37
+ }
38
+ });
39
+ it("reports size + mtime for files that exist", () => {
40
+ writeFileSync(join(workDir, "CLAUDE.md"), "hi");
41
+ const files = listProjectFiles(workDir);
42
+ const claude = files.find((f) => f.relPath === "CLAUDE.md");
43
+ expect(claude.exists).toBe(true);
44
+ expect(claude.size).toBe(2);
45
+ expect(typeof claude.mtime).toBe("number");
46
+ });
47
+ it("does not throw when workDir was deleted out from under us", () => {
48
+ rmSync(workDir, { recursive: true, force: true });
49
+ const files = listProjectFiles(workDir);
50
+ expect(files.every((f) => !f.exists)).toBe(true);
51
+ });
52
+ });
53
+ describe("readProjectFile", () => {
54
+ it("returns empty content + valid etag for a missing file", () => {
55
+ const r = readProjectFile(workDir, "CLAUDE.md");
56
+ expect(r.content).toBe("");
57
+ expect(r.etag).toMatch(/^[0-9a-f]{12}$/);
58
+ expect(r.mtime).toBe(0);
59
+ });
60
+ it("reads existing file content", () => {
61
+ writeFileSync(join(workDir, "CLAUDE.md"), "always reply in chinese\n");
62
+ const r = readProjectFile(workDir, "CLAUDE.md");
63
+ expect(r.content).toBe("always reply in chinese\n");
64
+ expect(r.mtime).toBeGreaterThan(0);
65
+ });
66
+ it("rejects non-whitelisted paths", () => {
67
+ expect(() => readProjectFile(workDir, "../etc/passwd")).toThrow(ProjectFilesError);
68
+ expect(() => readProjectFile(workDir, ".git/config")).toThrow(ProjectFilesError);
69
+ });
70
+ it("rejects symlinks pointing outside workDir", () => {
71
+ // Create an outside-workDir target that definitely exists.
72
+ const outside = mkdtempSync(join(tmpdir(), "clawnet-outside-"));
73
+ const target = join(outside, "leaked.md");
74
+ writeFileSync(target, "secret");
75
+ mkdirSync(join(workDir, ".claude"));
76
+ try {
77
+ symlinkSync(target, join(workDir, ".claude", "CLAUDE.md"));
78
+ }
79
+ catch {
80
+ rmSync(outside, { recursive: true, force: true });
81
+ return; // skip on platforms where we can't symlink
82
+ }
83
+ try {
84
+ expect(() => readProjectFile(workDir, ".claude/CLAUDE.md")).toThrow(ProjectFilesError);
85
+ }
86
+ finally {
87
+ rmSync(outside, { recursive: true, force: true });
88
+ }
89
+ });
90
+ });
91
+ describe("writeProjectFile", () => {
92
+ it("creates the file when it does not exist", () => {
93
+ const r = writeProjectFile(workDir, "CLAUDE.md", "hello");
94
+ expect(r.content).toBe("hello\n"); // md gets trailing newline
95
+ expect(readFileSync(join(workDir, "CLAUDE.md"), "utf-8")).toBe("hello\n");
96
+ });
97
+ it("creates .claude/ directory automatically", () => {
98
+ writeProjectFile(workDir, ".claude/settings.json", '{"foo":1}');
99
+ expect(existsSync(join(workDir, ".claude"))).toBe(true);
100
+ expect(readFileSync(join(workDir, ".claude", "settings.json"), "utf-8")).toBe('{"foo":1}');
101
+ });
102
+ it("rejects invalid JSON for .json files", () => {
103
+ expect(() => writeProjectFile(workDir, ".claude/settings.json", "{not json")).toThrow(ProjectFilesError);
104
+ expect(existsSync(join(workDir, ".claude", "settings.json"))).toBe(false);
105
+ });
106
+ it("normalizes empty JSON to '{}'", () => {
107
+ writeProjectFile(workDir, ".claude/settings.json", "");
108
+ const txt = readFileSync(join(workDir, ".claude", "settings.json"), "utf-8");
109
+ expect(txt).toBe("{}\n");
110
+ });
111
+ it("etag mismatch returns 409 with current content", () => {
112
+ writeProjectFile(workDir, "CLAUDE.md", "v1");
113
+ expect(() => writeProjectFile(workDir, "CLAUDE.md", "v2", { ifMatchEtag: "stale-etag" })).toThrow(ProjectFilesError);
114
+ try {
115
+ writeProjectFile(workDir, "CLAUDE.md", "v2", { ifMatchEtag: "stale-etag" });
116
+ }
117
+ catch (err) {
118
+ const e = err;
119
+ expect(e.status).toBe(409);
120
+ expect(e.details?.currentContent).toBe("v1\n");
121
+ }
122
+ });
123
+ it("etag match permits the write", () => {
124
+ const first = writeProjectFile(workDir, "CLAUDE.md", "v1");
125
+ const second = writeProjectFile(workDir, "CLAUDE.md", "v2", { ifMatchEtag: first.etag });
126
+ expect(second.content).toBe("v2\n");
127
+ expect(second.etag).not.toBe(first.etag);
128
+ });
129
+ it("rejects non-whitelisted writes", () => {
130
+ expect(() => writeProjectFile(workDir, "../escape.md", "x")).toThrow(ProjectFilesError);
131
+ });
132
+ it("write is atomic (no partial file visible if rename fails — smoke check via tmp cleanup)", () => {
133
+ writeProjectFile(workDir, "CLAUDE.md", "atomic");
134
+ // Confirm no `.tmp` siblings remain.
135
+ const entries = readFileSync(join(workDir, "CLAUDE.md"), "utf-8");
136
+ expect(entries).toBe("atomic\n");
137
+ // The tmp pattern is `.<hex>.tmp` — none should remain.
138
+ const dirents = require("node:fs").readdirSync(workDir);
139
+ for (const d of dirents)
140
+ expect(d).not.toMatch(/^\..*\.tmp$/);
141
+ });
142
+ });
143
+ //# sourceMappingURL=project-files.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.test.js","sourceRoot":"","sources":["../../src/__tests__/project-files.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=projects-fs.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/projects-fs.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { TaskStore, projectRoot } from "@mclawnet/task";
6
+ import { listAllProjectSummaries, loadProjectSummary, loadSwarmSummaries, resolveWorkDir, } from "../projects-fs.js";
7
+ let home;
8
+ let workDir;
9
+ beforeEach(async () => {
10
+ home = mkdtempSync(join(tmpdir(), "swarm-projects-fs-"));
11
+ workDir = mkdtempSync(join(tmpdir(), "swarm-projects-fs-wd-"));
12
+ const root = projectRoot(workDir, home);
13
+ mkdirSync(root, { recursive: true });
14
+ writeFileSync(join(root, "meta.json"), JSON.stringify({
15
+ version: 1, workDir, createdAt: "2026-05-09T00:00:00Z",
16
+ }));
17
+ await new TaskStore({ workDir, home }).create({
18
+ teamName: "dev", subject: "task A",
19
+ });
20
+ });
21
+ afterEach(() => {
22
+ rmSync(home, { recursive: true, force: true });
23
+ rmSync(workDir, { recursive: true, force: true });
24
+ });
25
+ describe("projects-fs", () => {
26
+ it("listAllProjectSummaries returns one summary with task count 1", () => {
27
+ const summaries = listAllProjectSummaries(home);
28
+ expect(summaries).toHaveLength(1);
29
+ expect(summaries[0].workDir).toBe(workDir);
30
+ expect(summaries[0].taskCount).toBe(1);
31
+ expect(summaries[0].taskCounts.pending).toBe(1);
32
+ });
33
+ it("resolveWorkDir round-trips an encodedCwd", () => {
34
+ const enc = listAllProjectSummaries(home)[0].encodedCwd;
35
+ expect(resolveWorkDir(home, enc)).toBe(workDir);
36
+ });
37
+ it("loadProjectSummary returns null for unknown encodedCwd", () => {
38
+ expect(loadProjectSummary(home, "-not-a-real-dir")).toBeNull();
39
+ });
40
+ it("loadSwarmSummaries returns [] when no swarms exist", () => {
41
+ expect(loadSwarmSummaries(home, workDir)).toEqual([]);
42
+ });
43
+ it("rejects unsafe encodedCwd containing slashes or ..", () => {
44
+ expect(resolveWorkDir(home, "../etc")).toBeNull();
45
+ expect(resolveWorkDir(home, "a/b")).toBeNull();
46
+ expect(resolveWorkDir(home, "")).toBeNull();
47
+ });
48
+ it("loadSwarmSummaries returns one summary mapping snapshot fields", () => {
49
+ // Build a minimal recovery.json snapshot under
50
+ // <projectRoot>/swarms/<swarmId>/recovery.json — this is the actual
51
+ // filename that loadSwarmSnapshot() reads (see persistence.ts).
52
+ // Note: persistence.ts derives its own home from CLAWNET_HOME, so we
53
+ // must set it here to point swarm-side resolution at our temp dir.
54
+ const swarmId = "swarm-test-1";
55
+ const swarmsDir = join(projectRoot(workDir, home), "swarms", swarmId);
56
+ mkdirSync(swarmsDir, { recursive: true });
57
+ const snapshot = {
58
+ id: swarmId,
59
+ hubSessionId: "hub-1",
60
+ workDir,
61
+ teamName: "dev",
62
+ roles: [
63
+ { instanceId: "queen-0", roleName: "queen", status: "ready" },
64
+ { instanceId: "worker-0", roleName: "worker", status: "ready" },
65
+ ],
66
+ plan: null,
67
+ nextInstanceSeq: {},
68
+ savedAt: 12345,
69
+ status: "active",
70
+ };
71
+ writeFileSync(join(swarmsDir, "recovery.json"), JSON.stringify(snapshot));
72
+ const prevHome = process.env.CLAWNET_HOME;
73
+ process.env.CLAWNET_HOME = home;
74
+ try {
75
+ const summaries = loadSwarmSummaries(home, workDir);
76
+ expect(summaries).toHaveLength(1);
77
+ expect(summaries[0]).toEqual({
78
+ swarmId,
79
+ teamName: "dev",
80
+ status: "active",
81
+ roleCount: 2,
82
+ savedAt: 12345,
83
+ });
84
+ }
85
+ finally {
86
+ if (prevHome === undefined)
87
+ delete process.env.CLAWNET_HOME;
88
+ else
89
+ process.env.CLAWNET_HOME = prevHome;
90
+ }
91
+ });
92
+ it("counts non-pending task statuses correctly", async () => {
93
+ // beforeEach already created one pending task. Add a second task and
94
+ // mutate it to in_progress via TaskStore.update() so the
95
+ // `if (t.status in counts)` branch at projects-fs.ts:90 is exercised
96
+ // for a non-pending status.
97
+ const store = new TaskStore({ workDir, home });
98
+ const second = await store.create({ teamName: "dev", subject: "task B" });
99
+ await store.update(second.id, { status: "in_progress" });
100
+ const summaries = listAllProjectSummaries(home);
101
+ expect(summaries).toHaveLength(1);
102
+ expect(summaries[0].taskCount).toBe(2);
103
+ expect(summaries[0].taskCounts.pending).toBe(1);
104
+ expect(summaries[0].taskCounts.in_progress).toBe(1);
105
+ });
106
+ });
107
+ //# sourceMappingURL=projects-fs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.test.js","sourceRoot":"","sources":["../../src/__tests__/projects-fs.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -13,4 +13,8 @@ export type { SessionAdapter, HubAdapter, SwarmInstance, RoleInstance, SwarmActi
13
13
  export type { RoleDefinition, DelegationMap, DelegationEntry } from "./roles/types.js";
14
14
  export { initTemplates, listTemplates, loadTemplate } from "./templates/template-loader.js";
15
15
  export type { TemplateDefinition, TemplateRoleSpec } from "./templates/types.js";
16
+ export { listProjectDirs, resolveWorkDir, loadProjectSummary, loadSwarmSummaries, listAllProjectSummaries, } from "./projects-fs.js";
17
+ export type { TaskStatus, TaskCounts, ProjectSummary, SwarmSummary, } from "./projects-fs.js";
18
+ export { PROJECT_FILE_WHITELIST, isWhitelistedRelPath, listProjectFiles, readProjectFile, writeProjectFile, ProjectFilesError, } from "./project-files.js";
19
+ export type { ProjectFileRelPath, ProjectFileEntry, ProjectFileContent, } from "./project-files.js";
16
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxG,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhG,YAAY,EACV,cAAc,EACd,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxG,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhG,YAAY,EACV,cAAc,EACd,UAAU,EACV,aAAa,EACb,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,WAAW,EACX,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC5F,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEjF,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -8,4 +8,6 @@ export { InboxStore } from "./inbox-store.js";
8
8
  export { InboxRelay } from "./inbox-relay.js";
9
9
  export { InboxWatcher } from "./inbox-watcher.js";
10
10
  export { initTemplates, listTemplates, loadTemplate } from "./templates/template-loader.js";
11
+ export { listProjectDirs, resolveWorkDir, loadProjectSummary, loadSwarmSummaries, listAllProjectSummaries, } from "./projects-fs.js";
12
+ export { PROJECT_FILE_WHITELIST, isWhitelistedRelPath, listProjectFiles, readProjectFile, writeProjectFile, ProjectFilesError, } from "./project-files.js";
11
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAE7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxG,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAgD,MAAM,oBAAoB,CAAC;AAiBhG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAE7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACxG,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAgD,MAAM,oBAAoB,CAAC;AAiBhG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAG5F,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAQ1B,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Project files endpoint helpers.
3
+ *
4
+ * Surfaces a fixed whitelist of files inside a project's `workDir` (CLAUDE.md
5
+ * and .claude/settings*.json) so the UI can read/write them without giving
6
+ * blanket FS access.
7
+ *
8
+ * Why a hard-coded whitelist (no globs in P1):
9
+ * The Claude CLI auto-loads exactly these paths from cwd. Limiting to the
10
+ * exact set lets us audit every read/write and rules out mistakes like a
11
+ * user typing `relPath=../../etc/passwd` or `.git/config`. Phase 2 may add
12
+ * `.claude/rules/*.md`, which will require a careful prefix-glob design.
13
+ *
14
+ * Defense-in-depth path checks:
15
+ * 1. relPath must be in WHITELIST (exact string match)
16
+ * 2. Resolved absolute path must live under realpath(workDir)
17
+ * 3. realpath() resolves symlinks, so a symlink inside .claude/ that
18
+ * points outside workDir is still rejected by check (2).
19
+ */
20
+ export declare const PROJECT_FILE_WHITELIST: readonly ["CLAUDE.md", ".claude/CLAUDE.md", ".claude/settings.json", ".claude/settings.local.json"];
21
+ export type ProjectFileRelPath = (typeof PROJECT_FILE_WHITELIST)[number];
22
+ export interface ProjectFileEntry {
23
+ relPath: ProjectFileRelPath;
24
+ exists: boolean;
25
+ size?: number;
26
+ mtime?: number;
27
+ loadedByCli: true;
28
+ }
29
+ export interface ProjectFileContent {
30
+ relPath: ProjectFileRelPath;
31
+ content: string;
32
+ mtime: number;
33
+ etag: string;
34
+ }
35
+ export declare class ProjectFilesError extends Error {
36
+ status: number;
37
+ details?: Record<string, unknown> | undefined;
38
+ constructor(status: number, message: string, details?: Record<string, unknown> | undefined);
39
+ }
40
+ /** Type guard — narrows an arbitrary string to the whitelist union. */
41
+ export declare function isWhitelistedRelPath(rel: string): rel is ProjectFileRelPath;
42
+ /** List the whitelist with stat info for present files. Never throws. */
43
+ export declare function listProjectFiles(workDir: string): ProjectFileEntry[];
44
+ /**
45
+ * Read a whitelisted file. Returns content="" + etag of empty string when
46
+ * the file does not exist, so the editor UI can populate a blank textarea
47
+ * with a valid etag for first-write conflict detection.
48
+ */
49
+ export declare function readProjectFile(workDir: string, relPath: string): ProjectFileContent;
50
+ export interface WriteOptions {
51
+ ifMatchEtag?: string;
52
+ }
53
+ /**
54
+ * Atomic write: validates JSON syntax for `.json` files, ensures parent
55
+ * `.claude/` exists, writes to a tmp sibling, then renames. Returns the
56
+ * post-write content snapshot. On etag mismatch throws 409 with the current
57
+ * server-side content so the UI can render a 3-way diff.
58
+ */
59
+ export declare function writeProjectFile(workDir: string, relPath: string, newContent: string, opts?: WriteOptions): ProjectFileContent;
60
+ //# sourceMappingURL=project-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.d.ts","sourceRoot":"","sources":["../src/project-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,eAAO,MAAM,sBAAsB,qGAKzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAEjC,MAAM,EAAE,MAAM;IAEd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFjC,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACR,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA;CAK3C;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,kBAAkB,CAE3E;AA8CD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmCpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAmBpF;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,YAAiB,GACtB,kBAAkB,CA6DpB"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Project files endpoint helpers.
3
+ *
4
+ * Surfaces a fixed whitelist of files inside a project's `workDir` (CLAUDE.md
5
+ * and .claude/settings*.json) so the UI can read/write them without giving
6
+ * blanket FS access.
7
+ *
8
+ * Why a hard-coded whitelist (no globs in P1):
9
+ * The Claude CLI auto-loads exactly these paths from cwd. Limiting to the
10
+ * exact set lets us audit every read/write and rules out mistakes like a
11
+ * user typing `relPath=../../etc/passwd` or `.git/config`. Phase 2 may add
12
+ * `.claude/rules/*.md`, which will require a careful prefix-glob design.
13
+ *
14
+ * Defense-in-depth path checks:
15
+ * 1. relPath must be in WHITELIST (exact string match)
16
+ * 2. Resolved absolute path must live under realpath(workDir)
17
+ * 3. realpath() resolves symlinks, so a symlink inside .claude/ that
18
+ * points outside workDir is still rejected by check (2).
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync, } from "node:fs";
21
+ import { createHash, randomBytes } from "node:crypto";
22
+ import { dirname, join, sep } from "node:path";
23
+ export const PROJECT_FILE_WHITELIST = [
24
+ "CLAUDE.md",
25
+ ".claude/CLAUDE.md",
26
+ ".claude/settings.json",
27
+ ".claude/settings.local.json",
28
+ ];
29
+ export class ProjectFilesError extends Error {
30
+ status;
31
+ details;
32
+ constructor(status, message, details) {
33
+ super(message);
34
+ this.status = status;
35
+ this.details = details;
36
+ this.name = "ProjectFilesError";
37
+ }
38
+ }
39
+ /** Type guard — narrows an arbitrary string to the whitelist union. */
40
+ export function isWhitelistedRelPath(rel) {
41
+ return PROJECT_FILE_WHITELIST.includes(rel);
42
+ }
43
+ /**
44
+ * Resolve `relPath` against `workDir` and verify the result is still inside
45
+ * workDir even after symlink resolution. Throws ProjectFilesError on any
46
+ * violation. Returns the absolute path (which may not exist yet — caller
47
+ * decides whether absence is OK).
48
+ */
49
+ function resolveSafe(workDir, relPath) {
50
+ // workDir itself must exist for any of this to make sense.
51
+ let workDirReal;
52
+ try {
53
+ workDirReal = realpathSync(workDir);
54
+ }
55
+ catch {
56
+ throw new ProjectFilesError(404, "workDir does not exist on disk");
57
+ }
58
+ const candidate = join(workDirReal, relPath);
59
+ // For an existing file, realpath the file. For a not-yet-existing file,
60
+ // realpath the *parent* (which we may need to mkdir later) — this still
61
+ // catches symlink escapes from intermediate components.
62
+ let probe;
63
+ if (existsSync(candidate)) {
64
+ probe = realpathSync(candidate);
65
+ }
66
+ else {
67
+ // Walk up to the first existing ancestor and realpath it; that's enough
68
+ // to ensure we won't end up writing outside workDir even after rename().
69
+ let parent = dirname(candidate);
70
+ while (!existsSync(parent))
71
+ parent = dirname(parent);
72
+ probe = realpathSync(parent);
73
+ }
74
+ if (probe !== workDirReal && !probe.startsWith(workDirReal + sep)) {
75
+ throw new ProjectFilesError(403, "Path resolves outside workDir", {
76
+ relPath,
77
+ });
78
+ }
79
+ return candidate;
80
+ }
81
+ function computeEtag(content) {
82
+ return createHash("sha256").update(content).digest("hex").slice(0, 12);
83
+ }
84
+ /** List the whitelist with stat info for present files. Never throws. */
85
+ export function listProjectFiles(workDir) {
86
+ const out = [];
87
+ for (const rel of PROJECT_FILE_WHITELIST) {
88
+ let abs;
89
+ try {
90
+ abs = resolveSafe(workDir, rel);
91
+ }
92
+ catch {
93
+ // workDir missing or path escape — surface as "not exists" rather than
94
+ // erroring the whole list call (e.g. a project whose workDir was deleted
95
+ // on disk should still be navigable).
96
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
97
+ continue;
98
+ }
99
+ if (!existsSync(abs)) {
100
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
101
+ continue;
102
+ }
103
+ try {
104
+ const st = statSync(abs);
105
+ if (!st.isFile()) {
106
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
107
+ continue;
108
+ }
109
+ out.push({
110
+ relPath: rel,
111
+ exists: true,
112
+ size: st.size,
113
+ mtime: st.mtime.getTime(),
114
+ loadedByCli: true,
115
+ });
116
+ }
117
+ catch {
118
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * Read a whitelisted file. Returns content="" + etag of empty string when
125
+ * the file does not exist, so the editor UI can populate a blank textarea
126
+ * with a valid etag for first-write conflict detection.
127
+ */
128
+ export function readProjectFile(workDir, relPath) {
129
+ if (!isWhitelistedRelPath(relPath)) {
130
+ throw new ProjectFilesError(400, "relPath not in whitelist", { relPath });
131
+ }
132
+ const abs = resolveSafe(workDir, relPath);
133
+ if (!existsSync(abs)) {
134
+ return { relPath, content: "", mtime: 0, etag: computeEtag("") };
135
+ }
136
+ const st = statSync(abs);
137
+ if (!st.isFile()) {
138
+ throw new ProjectFilesError(400, "Path is not a regular file", { relPath });
139
+ }
140
+ const content = readFileSync(abs, "utf-8");
141
+ return {
142
+ relPath,
143
+ content,
144
+ mtime: st.mtime.getTime(),
145
+ etag: computeEtag(content),
146
+ };
147
+ }
148
+ /**
149
+ * Atomic write: validates JSON syntax for `.json` files, ensures parent
150
+ * `.claude/` exists, writes to a tmp sibling, then renames. Returns the
151
+ * post-write content snapshot. On etag mismatch throws 409 with the current
152
+ * server-side content so the UI can render a 3-way diff.
153
+ */
154
+ export function writeProjectFile(workDir, relPath, newContent, opts = {}) {
155
+ if (!isWhitelistedRelPath(relPath)) {
156
+ throw new ProjectFilesError(400, "relPath not in whitelist", { relPath });
157
+ }
158
+ // Optimistic-concurrency check happens BEFORE any FS mutation.
159
+ if (opts.ifMatchEtag !== undefined) {
160
+ const current = readProjectFile(workDir, relPath);
161
+ if (current.etag !== opts.ifMatchEtag) {
162
+ throw new ProjectFilesError(409, "etag mismatch", {
163
+ relPath,
164
+ currentEtag: current.etag,
165
+ currentContent: current.content,
166
+ currentMtime: current.mtime,
167
+ });
168
+ }
169
+ }
170
+ // Validate JSON before touching disk so we never leave broken settings.json.
171
+ if (relPath.endsWith(".json")) {
172
+ if (newContent.trim().length === 0) {
173
+ // Empty JSON → store as `{}` so the file remains valid for CLI consumers.
174
+ newContent = "{}\n";
175
+ }
176
+ else {
177
+ try {
178
+ JSON.parse(newContent);
179
+ }
180
+ catch (err) {
181
+ throw new ProjectFilesError(400, "Invalid JSON", {
182
+ relPath,
183
+ parseError: err instanceof Error ? err.message : String(err),
184
+ });
185
+ }
186
+ }
187
+ }
188
+ else if (relPath.endsWith(".md")) {
189
+ // Ensure trailing newline — POSIX text-file convention; avoids editors
190
+ // appending a newline on next manual edit and producing spurious diffs.
191
+ if (newContent.length > 0 && !newContent.endsWith("\n")) {
192
+ newContent = newContent + "\n";
193
+ }
194
+ }
195
+ const abs = resolveSafe(workDir, relPath);
196
+ const parent = dirname(abs);
197
+ if (!existsSync(parent)) {
198
+ mkdirSync(parent, { recursive: true });
199
+ }
200
+ // tmp file lives in the same directory so rename() is atomic on the same FS.
201
+ // randomBytes avoids collisions if two writes race; the loser gets EEXIST on
202
+ // its own tmp (very unlikely) and retries via the API layer.
203
+ const tmp = join(parent, `.${randomBytes(6).toString("hex")}.tmp`);
204
+ writeFileSync(tmp, newContent, "utf-8");
205
+ renameSync(tmp, abs);
206
+ const st = statSync(abs);
207
+ return {
208
+ relPath,
209
+ content: newContent,
210
+ mtime: st.mtime.getTime(),
211
+ etag: computeEtag(newContent),
212
+ };
213
+ }
214
+ //# sourceMappingURL=project-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.js","sourceRoot":"","sources":["../src/project-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
2
+ export interface TaskCounts {
3
+ pending: number;
4
+ in_progress: number;
5
+ completed: number;
6
+ cancelled: number;
7
+ }
8
+ export interface ProjectSummary {
9
+ encodedCwd: string;
10
+ workDir: string;
11
+ createdAt: string | null;
12
+ swarmCount: number;
13
+ taskCount: number;
14
+ taskCounts: TaskCounts;
15
+ }
16
+ export interface SwarmSummary {
17
+ swarmId: string;
18
+ teamName: string;
19
+ status: string;
20
+ roleCount: number;
21
+ savedAt: number | null;
22
+ }
23
+ export declare function listProjectDirs(home?: string): string[];
24
+ export declare function resolveWorkDir(home: string, encodedCwd: string): string | null;
25
+ export declare function loadProjectSummary(home: string, encodedCwd: string): ProjectSummary | null;
26
+ export declare function loadSwarmSummaries(home: string, workDir: string): SwarmSummary[];
27
+ export declare function listAllProjectSummaries(home?: string): ProjectSummary[];
28
+ //# sourceMappingURL=projects-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.d.ts","sourceRoot":"","sources":["../src/projects-fs.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;AAC/E,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;CACxB;AACD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAkBD,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY9E;AAiBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAkB1F;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAiBhF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvE"}
@@ -0,0 +1,111 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { TaskStore, projectRoot } from "@mclawnet/task";
5
+ import { loadSwarmSnapshot } from "./persistence.js";
6
+ function getHome(homeOverride) {
7
+ return homeOverride ?? process.env.CLAWNET_HOME ?? homedir();
8
+ }
9
+ function projectsDir(home) {
10
+ return join(home, ".clawnet", "projects");
11
+ }
12
+ function isSafeEncoded(encoded) {
13
+ if (!encoded)
14
+ return false;
15
+ if (encoded.includes("/") || encoded.includes("\\"))
16
+ return false;
17
+ if (encoded.includes("\0"))
18
+ return false;
19
+ if (encoded === "." || encoded === ".." || encoded.includes(".."))
20
+ return false;
21
+ return true;
22
+ }
23
+ export function listProjectDirs(home) {
24
+ const dir = projectsDir(getHome(home));
25
+ if (!existsSync(dir))
26
+ return [];
27
+ return readdirSync(dir, { withFileTypes: true })
28
+ .filter((e) => e.isDirectory())
29
+ .map((e) => e.name);
30
+ }
31
+ export function resolveWorkDir(home, encodedCwd) {
32
+ if (!isSafeEncoded(encodedCwd))
33
+ return null;
34
+ const dirs = listProjectDirs(home);
35
+ if (!dirs.includes(encodedCwd))
36
+ return null;
37
+ const metaPath = join(projectsDir(home), encodedCwd, "meta.json");
38
+ if (!existsSync(metaPath))
39
+ return null;
40
+ try {
41
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
42
+ return typeof meta.workDir === "string" ? meta.workDir : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function readMetaCreatedAt(home, encodedCwd) {
49
+ const metaPath = join(projectsDir(home), encodedCwd, "meta.json");
50
+ if (!existsSync(metaPath))
51
+ return null;
52
+ try {
53
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
54
+ return meta.createdAt ?? null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ function emptyCounts() {
61
+ return { pending: 0, in_progress: 0, completed: 0, cancelled: 0 };
62
+ }
63
+ export function loadProjectSummary(home, encodedCwd) {
64
+ const workDir = resolveWorkDir(home, encodedCwd);
65
+ if (!workDir)
66
+ return null;
67
+ const store = new TaskStore({ workDir, home });
68
+ const tasks = store.list();
69
+ const counts = emptyCounts();
70
+ for (const t of tasks) {
71
+ if (t.status in counts)
72
+ counts[t.status] += 1;
73
+ }
74
+ const swarms = loadSwarmSummaries(home, workDir);
75
+ return {
76
+ encodedCwd,
77
+ workDir,
78
+ createdAt: readMetaCreatedAt(home, encodedCwd),
79
+ swarmCount: swarms.length,
80
+ taskCount: tasks.length,
81
+ taskCounts: counts,
82
+ };
83
+ }
84
+ export function loadSwarmSummaries(home, workDir) {
85
+ const swarmsRoot = join(projectRoot(workDir, home), "swarms");
86
+ if (!existsSync(swarmsRoot))
87
+ return [];
88
+ const out = [];
89
+ for (const entry of readdirSync(swarmsRoot, { withFileTypes: true })) {
90
+ if (!entry.isDirectory())
91
+ continue;
92
+ const snap = loadSwarmSnapshot(workDir, entry.name);
93
+ if (!snap)
94
+ continue;
95
+ out.push({
96
+ swarmId: snap.id,
97
+ teamName: snap.teamName,
98
+ status: snap.status ?? "unknown",
99
+ roleCount: snap.roles?.length ?? 0,
100
+ savedAt: snap.savedAt ?? null,
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ export function listAllProjectSummaries(home) {
106
+ const h = getHome(home);
107
+ return listProjectDirs(h)
108
+ .map((e) => loadProjectSummary(h, e))
109
+ .filter((s) => !!s);
110
+ }
111
+ //# sourceMappingURL=projects-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.js","sourceRoot":"","sources":["../src/projects-fs.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mclawnet/swarm",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -19,10 +19,10 @@
19
19
  "dependencies": {
20
20
  "proper-lockfile": "^4.1.2",
21
21
  "@mclawnet/logger": "0.1.6",
22
+ "@mclawnet/memory": "0.1.5",
22
23
  "@mclawnet/skill-manager": "0.1.4",
23
- "@mclawnet/task": "0.1.1",
24
- "@mclawnet/shared": "0.1.3",
25
- "@mclawnet/memory": "0.1.5"
24
+ "@mclawnet/shared": "0.1.4",
25
+ "@mclawnet/task": "0.1.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/better-sqlite3": "^7",