sfora-cli 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/dist/SforaFs.js +270 -4
- package/dist/api-client.d.ts +47 -1
- package/dist/api-client.js +60 -3
- package/dist/cli.js +24 -11
- package/dist/format/__tests__/byteStable.d.ts +5 -0
- package/dist/format/__tests__/byteStable.js +64 -0
- package/dist/format/blocks/dropClosure.d.ts +72 -0
- package/dist/format/blocks/dropClosure.js +186 -0
- package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
- package/dist/format/blocks/markdown-block-catalog.js +162 -0
- package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
- package/dist/format/blocks/markdown-block-ids.mjs +25 -0
- package/dist/format/blocks/parsers.d.ts +105 -0
- package/dist/format/blocks/parsers.js +442 -0
- package/dist/format/blocks/structured-block-schema.d.ts +8 -0
- package/dist/format/blocks/structured-block-schema.js +30 -0
- package/dist/format/callout.d.ts +66 -0
- package/dist/format/callout.js +130 -0
- package/dist/format/cardMarkdown.d.ts +4 -0
- package/dist/format/cardMarkdown.js +12 -0
- package/dist/format/checklist.d.ts +34 -0
- package/dist/format/checklist.js +151 -0
- package/dist/format/index.d.ts +18 -4
- package/dist/format/index.js +24 -4
- package/dist/format/lineGeometry.d.ts +70 -0
- package/dist/format/lineGeometry.js +324 -0
- package/dist/format/lint/index.d.ts +20 -0
- package/dist/format/lint/index.js +22 -0
- package/dist/format/lint/lintSource.d.ts +36 -0
- package/dist/format/lint/lintSource.js +154 -0
- package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/broken-wiki-link.js +45 -0
- package/dist/format/lint/rules/index.d.ts +10 -0
- package/dist/format/lint/rules/index.js +26 -0
- package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
- package/dist/format/lint/rules/malformed-callout.js +79 -0
- package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
- package/dist/format/lint/rules/malformed-checklist.js +60 -0
- package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
- package/dist/format/lint/rules/malformed-frontmatter.js +93 -0
- package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
- package/dist/format/lint/rules/malformed-structured-block.js +134 -0
- package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
- package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
- package/dist/format/lint/rules/orphan-reference.js +87 -0
- package/dist/format/lint/types.d.ts +80 -0
- package/dist/format/lint/types.js +16 -0
- package/dist/format/markdown/dates.js +2 -0
- package/dist/format/markdown/document.js +2 -0
- package/dist/format/markdown/index.js +2 -0
- package/dist/format/markdown/mentions.js +2 -0
- package/dist/format/markdown/slug.js +2 -0
- package/dist/format/markdown/yaml.js +2 -0
- package/dist/format/noteMarkdown.js +2 -0
- package/dist/format/parseWithFallback.d.ts +13 -0
- package/dist/format/parseWithFallback.js +98 -0
- package/dist/format/plaintext.d.ts +5 -0
- package/dist/format/plaintext.js +41 -0
- package/dist/format/postMarkdown.js +3 -1
- package/dist/format/taskUploadFilename.d.ts +6 -0
- package/dist/format/taskUploadFilename.js +13 -0
- package/dist/format/wayfinder.d.ts +50 -0
- package/dist/format/wayfinder.js +203 -0
- package/dist/format/wikiLinks.d.ts +19 -0
- package/dist/format/wikiLinks.js +80 -0
- package/dist/local/workspace.d.ts +12 -0
- package/dist/local/workspace.js +100 -7
- package/dist/mcp-server.js +11 -5
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ shell interpreter backs the interactive mode).
|
|
|
21
21
|
├── projects/<slug>/posts/<YYYY-MM-DD-title>.md # published posts (GET·PUT·DELETE)
|
|
22
22
|
│ /drafts/<…>.md # your drafts (GET·PUT·DELETE)
|
|
23
23
|
│ /board/<NN-col>/<NNNN-card>.md # tasks by column (GET·PUT·DELETE)
|
|
24
|
-
│ /
|
|
24
|
+
│ /library/documents/<…>.md # docs / notes (GET·PUT·DELETE)
|
|
25
|
+
│ /files/<…> # uploaded files (GET)
|
|
26
|
+
│ /repositories/<repo>/… # source trees (GET)
|
|
25
27
|
├── inbox/mentions.md # unread mentions (GET)
|
|
26
28
|
└── me/api-key # your identity (GET)
|
|
27
29
|
```
|
|
@@ -154,11 +156,11 @@ sfora:/$ cd /projects/general/posts # cwd persists across commands
|
|
|
154
156
|
sfora:/projects/general/posts$ exit
|
|
155
157
|
```
|
|
156
158
|
|
|
157
|
-
Writing a file
|
|
158
|
-
|
|
159
|
-
is
|
|
160
|
-
|
|
161
|
-
schedules auto-publish.
|
|
159
|
+
Writing a post file publishes a new immutable record. Edit mutable work under
|
|
160
|
+
`…/drafts/` and publish only when it is ready; overwriting an existing published
|
|
161
|
+
filename is rejected. A bare `@Display Name` that matches an active member is
|
|
162
|
+
rehydrated to a real mention server-side. `rm` soft-deletes (author or org
|
|
163
|
+
admin/owner). A `scheduledFor:` in draft frontmatter schedules auto-publish.
|
|
162
164
|
|
|
163
165
|
## MCP server (Claude Desktop / Cursor)
|
|
164
166
|
|
package/dist/SforaFs.js
CHANGED
|
@@ -79,6 +79,15 @@ function classify(path) {
|
|
|
79
79
|
if (dir === "links.md" && seg.length === 3) {
|
|
80
80
|
return { kind: "linksFile", slug };
|
|
81
81
|
}
|
|
82
|
+
if (dir === "plan.md" && seg.length === 3) {
|
|
83
|
+
return { kind: "planFile", slug };
|
|
84
|
+
}
|
|
85
|
+
if (dir === "map.md" && seg.length === 3) {
|
|
86
|
+
return { kind: "mapFile", slug };
|
|
87
|
+
}
|
|
88
|
+
if (dir === "asks.md" && seg.length === 3) {
|
|
89
|
+
return { kind: "asksFile", slug };
|
|
90
|
+
}
|
|
82
91
|
if (dir === "posts" || dir === "drafts") {
|
|
83
92
|
if (seg.length === 3)
|
|
84
93
|
return { kind: "postsDir", slug, dir };
|
|
@@ -92,6 +101,38 @@ function classify(path) {
|
|
|
92
101
|
if (seg.length === 4)
|
|
93
102
|
return { kind: "docFile", slug, filename: seg[3] };
|
|
94
103
|
}
|
|
104
|
+
if (dir === "artifacts") {
|
|
105
|
+
if (seg.length === 3)
|
|
106
|
+
return { kind: "filesDir", slug };
|
|
107
|
+
if (seg.length === 4)
|
|
108
|
+
return { kind: "artifactFile", slug, filename: seg[3] };
|
|
109
|
+
}
|
|
110
|
+
if (dir === "library") {
|
|
111
|
+
if (seg.length === 3)
|
|
112
|
+
return { kind: "libraryDir", slug };
|
|
113
|
+
if (seg[3] === "documents") {
|
|
114
|
+
if (seg.length === 4)
|
|
115
|
+
return { kind: "docsDir", slug };
|
|
116
|
+
if (seg.length === 5)
|
|
117
|
+
return { kind: "docFile", slug, filename: seg[4] };
|
|
118
|
+
}
|
|
119
|
+
if (seg[3] === "files") {
|
|
120
|
+
if (seg.length === 4)
|
|
121
|
+
return { kind: "filesDir", slug };
|
|
122
|
+
if (seg.length === 5)
|
|
123
|
+
return { kind: "artifactFile", slug, filename: seg[4] };
|
|
124
|
+
}
|
|
125
|
+
if (seg[3] === "repositories") {
|
|
126
|
+
if (seg.length === 4)
|
|
127
|
+
return { kind: "repositoriesDir", slug };
|
|
128
|
+
return {
|
|
129
|
+
kind: "repositoryPath",
|
|
130
|
+
slug,
|
|
131
|
+
repository: seg[4],
|
|
132
|
+
path: seg.slice(5).join("/"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
95
136
|
if (dir === "pulls") {
|
|
96
137
|
if (seg.length === 3)
|
|
97
138
|
return { kind: "pullsDir", slug };
|
|
@@ -212,9 +253,45 @@ export class SforaFs {
|
|
|
212
253
|
return this.#cached(key, async () => {
|
|
213
254
|
const notes = await this.#client.listNotes(slug);
|
|
214
255
|
this.#entrySnapshots.set(`/projects/${slug}/docs`, notes.map((n) => n.filename));
|
|
256
|
+
this.#entrySnapshots.set(`/projects/${slug}/library/documents`, notes.map((n) => n.filename));
|
|
215
257
|
return notes;
|
|
216
258
|
});
|
|
217
259
|
}
|
|
260
|
+
#invalidateNotes(slug) {
|
|
261
|
+
this.#cache.delete(`docs:${slug}`);
|
|
262
|
+
}
|
|
263
|
+
#listArtifacts(slug) {
|
|
264
|
+
return this.#cached(`artifacts:${slug}`, async () => {
|
|
265
|
+
const artifacts = await this.#client.listArtifacts(slug);
|
|
266
|
+
const names = artifacts.map((artifact) => artifact.name);
|
|
267
|
+
this.#entrySnapshots.set(`/projects/${slug}/artifacts`, names);
|
|
268
|
+
this.#entrySnapshots.set(`/projects/${slug}/library/files`, names);
|
|
269
|
+
return artifacts;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
#listRepositories(slug) {
|
|
273
|
+
return this.#cached(`repositories:${slug}`, async () => {
|
|
274
|
+
const repositories = await this.#client.listRepositories(slug);
|
|
275
|
+
this.#entrySnapshots.set(`/projects/${slug}/library/repositories`, repositories.map((repository) => repository.dirname));
|
|
276
|
+
return repositories;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
#repositoryTree(slug, repository) {
|
|
280
|
+
return this.#cached(`repository-tree:${slug}/${repository}`, async () => {
|
|
281
|
+
const tree = await this.#client.getRepositoryTree(slug, repository);
|
|
282
|
+
this.#entrySnapshots.set(`/projects/${slug}/library/repositories/${repository}`, tree.entries.map((entry) => entry.path));
|
|
283
|
+
return tree;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async #repositoryEntry(slug, repository, path) {
|
|
287
|
+
if (!path) {
|
|
288
|
+
const repositories = await this.#listRepositories(slug);
|
|
289
|
+
return repositories.some((candidate) => candidate.dirname === repository)
|
|
290
|
+
? { kind: "directory" }
|
|
291
|
+
: undefined;
|
|
292
|
+
}
|
|
293
|
+
return (await this.#repositoryTree(slug, repository)).entries.find((entry) => entry.path === path);
|
|
294
|
+
}
|
|
218
295
|
// ─── pull requests cache (read-only) ─────────────────────────────
|
|
219
296
|
#listPulls(slug) {
|
|
220
297
|
const key = `pulls:${slug}`;
|
|
@@ -381,6 +458,31 @@ export class SforaFs {
|
|
|
381
458
|
catch (e) {
|
|
382
459
|
throw fromApi(e, "open", normalize(path));
|
|
383
460
|
}
|
|
461
|
+
case "artifactFile":
|
|
462
|
+
try {
|
|
463
|
+
return await this.#client.readArtifact(loc.slug, loc.filename);
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
throw fromApi(e, "open", normalize(path));
|
|
467
|
+
}
|
|
468
|
+
case "repositoryPath": {
|
|
469
|
+
if (!loc.path)
|
|
470
|
+
throw eisdir("read", normalize(path));
|
|
471
|
+
const entry = await this.#repositoryEntry(loc.slug, loc.repository, loc.path);
|
|
472
|
+
if (!entry)
|
|
473
|
+
throw enoent("open", normalize(path));
|
|
474
|
+
if (entry.kind === "directory")
|
|
475
|
+
throw eisdir("read", normalize(path));
|
|
476
|
+
if (entry.kind === "submodule") {
|
|
477
|
+
return `Submodule ${loc.path}\nRepository: ${loc.repository}\n`;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
return await this.#client.readRepositoryFile(loc.slug, loc.repository, loc.path);
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
throw fromApi(e, "open", normalize(path));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
384
486
|
case "pullFile":
|
|
385
487
|
try {
|
|
386
488
|
return await this.#client.readPull(loc.slug, loc.number);
|
|
@@ -416,6 +518,27 @@ export class SforaFs {
|
|
|
416
518
|
catch (e) {
|
|
417
519
|
throw fromApi(e, "open", normalize(path));
|
|
418
520
|
}
|
|
521
|
+
case "planFile":
|
|
522
|
+
try {
|
|
523
|
+
return await this.#client.readPlan(loc.slug);
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
throw fromApi(e, "open", normalize(path));
|
|
527
|
+
}
|
|
528
|
+
case "mapFile":
|
|
529
|
+
try {
|
|
530
|
+
return await this.#client.readMap(loc.slug);
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
throw fromApi(e, "open", normalize(path));
|
|
534
|
+
}
|
|
535
|
+
case "asksFile":
|
|
536
|
+
try {
|
|
537
|
+
return await this.#client.readAsks(loc.slug);
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
throw fromApi(e, "open", normalize(path));
|
|
541
|
+
}
|
|
419
542
|
case "root":
|
|
420
543
|
case "projectsDir":
|
|
421
544
|
case "inboxDir":
|
|
@@ -423,6 +546,9 @@ export class SforaFs {
|
|
|
423
546
|
case "projectDir":
|
|
424
547
|
case "postsDir":
|
|
425
548
|
case "docsDir":
|
|
549
|
+
case "libraryDir":
|
|
550
|
+
case "filesDir":
|
|
551
|
+
case "repositoriesDir":
|
|
426
552
|
case "pullsDir":
|
|
427
553
|
case "boardDir":
|
|
428
554
|
case "boardColumnDir":
|
|
@@ -449,6 +575,10 @@ export class SforaFs {
|
|
|
449
575
|
if (loc.kind === "inboxFile" || loc.kind === "meFile") {
|
|
450
576
|
throw eacces("open", norm);
|
|
451
577
|
}
|
|
578
|
+
// asks.md is a read-only projection — claims go through the asks API.
|
|
579
|
+
if (loc.kind === "asksFile") {
|
|
580
|
+
throw eacces("open", norm);
|
|
581
|
+
}
|
|
452
582
|
if (loc.kind === "linksFile") {
|
|
453
583
|
try {
|
|
454
584
|
await this.#client.setProjectLinks(loc.slug, toText(content));
|
|
@@ -458,6 +588,17 @@ export class SforaFs {
|
|
|
458
588
|
}
|
|
459
589
|
return;
|
|
460
590
|
}
|
|
591
|
+
// plan.md: the PUT edits `## the goal` only; the server ignores (and
|
|
592
|
+
// names) every generated section, so a read-modify-write is always safe.
|
|
593
|
+
if (loc.kind === "planFile") {
|
|
594
|
+
try {
|
|
595
|
+
await this.#client.writePlan(loc.slug, toText(content));
|
|
596
|
+
}
|
|
597
|
+
catch (e) {
|
|
598
|
+
throw fromApi(e, "open", norm);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
461
602
|
if (loc.kind === "root" ||
|
|
462
603
|
loc.kind === "projectsDir" ||
|
|
463
604
|
loc.kind === "inboxDir" ||
|
|
@@ -465,12 +606,21 @@ export class SforaFs {
|
|
|
465
606
|
loc.kind === "projectDir" ||
|
|
466
607
|
loc.kind === "postsDir" ||
|
|
467
608
|
loc.kind === "docsDir" ||
|
|
609
|
+
loc.kind === "libraryDir" ||
|
|
610
|
+
loc.kind === "filesDir" ||
|
|
611
|
+
loc.kind === "repositoriesDir" ||
|
|
468
612
|
loc.kind === "pullsDir" ||
|
|
469
613
|
loc.kind === "boardDir" ||
|
|
470
614
|
loc.kind === "boardColumnDir" ||
|
|
471
615
|
loc.kind === "publicDir") {
|
|
472
616
|
throw eisdir("open", norm);
|
|
473
617
|
}
|
|
618
|
+
// map.md is derived from the board and refuses writes server-side (405);
|
|
619
|
+
// fail locally with the same message so an agent learns the write doors
|
|
620
|
+
// without a round trip.
|
|
621
|
+
if (loc.kind === "mapFile") {
|
|
622
|
+
throw eacces("open", norm);
|
|
623
|
+
}
|
|
474
624
|
if (loc.kind === "postFile") {
|
|
475
625
|
try {
|
|
476
626
|
await this.#client.writePost(loc.slug, loc.dir, loc.filename, toText(content));
|
|
@@ -481,6 +631,16 @@ export class SforaFs {
|
|
|
481
631
|
this.#invalidate(loc.slug, loc.dir);
|
|
482
632
|
return;
|
|
483
633
|
}
|
|
634
|
+
if (loc.kind === "docFile") {
|
|
635
|
+
try {
|
|
636
|
+
await this.#client.writeNote(loc.slug, loc.filename, toText(content));
|
|
637
|
+
}
|
|
638
|
+
catch (e) {
|
|
639
|
+
throw fromApi(e, "open", norm);
|
|
640
|
+
}
|
|
641
|
+
this.#invalidateNotes(loc.slug);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
484
644
|
if (loc.kind === "boardCardFile") {
|
|
485
645
|
try {
|
|
486
646
|
const res = await this.#client.writeCard(loc.slug, loc.column, loc.filename, toText(content));
|
|
@@ -520,8 +680,14 @@ export class SforaFs {
|
|
|
520
680
|
case "projectDir":
|
|
521
681
|
case "postsDir":
|
|
522
682
|
case "docsDir":
|
|
683
|
+
case "libraryDir":
|
|
684
|
+
case "filesDir":
|
|
685
|
+
case "repositoriesDir":
|
|
523
686
|
case "pullsDir":
|
|
524
687
|
case "boardDir":
|
|
688
|
+
case "planFile":
|
|
689
|
+
case "mapFile":
|
|
690
|
+
case "asksFile":
|
|
525
691
|
return await this.#projectExists(loc.slug);
|
|
526
692
|
case "publicDir":
|
|
527
693
|
case "roadmapFile":
|
|
@@ -536,6 +702,10 @@ export class SforaFs {
|
|
|
536
702
|
undefined;
|
|
537
703
|
case "docFile":
|
|
538
704
|
return (await this.#matchNote(loc.slug, loc.filename)) !== undefined;
|
|
705
|
+
case "artifactFile":
|
|
706
|
+
return (await this.#listArtifacts(loc.slug)).some((artifact) => artifact.name === loc.filename);
|
|
707
|
+
case "repositoryPath":
|
|
708
|
+
return (await this.#repositoryEntry(loc.slug, loc.repository, loc.path)) !== undefined;
|
|
539
709
|
case "pullFile":
|
|
540
710
|
return (await this.#matchPull(loc.slug, loc.number)) !== undefined;
|
|
541
711
|
case "boardCardFile":
|
|
@@ -583,12 +753,18 @@ export class SforaFs {
|
|
|
583
753
|
case "meFile":
|
|
584
754
|
return this.#fileStat(new Date());
|
|
585
755
|
case "linksFile":
|
|
756
|
+
case "planFile":
|
|
757
|
+
case "mapFile":
|
|
758
|
+
case "asksFile":
|
|
586
759
|
if (!(await this.#projectExists(loc.slug)))
|
|
587
760
|
throw enoent("stat", norm);
|
|
588
761
|
return this.#fileStat(new Date());
|
|
589
762
|
case "projectDir":
|
|
590
763
|
case "postsDir":
|
|
591
764
|
case "docsDir":
|
|
765
|
+
case "libraryDir":
|
|
766
|
+
case "filesDir":
|
|
767
|
+
case "repositoriesDir":
|
|
592
768
|
case "pullsDir":
|
|
593
769
|
case "boardDir":
|
|
594
770
|
if (!(await this.#projectExists(loc.slug)))
|
|
@@ -625,6 +801,20 @@ export class SforaFs {
|
|
|
625
801
|
throw enoent("stat", norm);
|
|
626
802
|
return this.#fileStat(new Date(note.lastEditedAt || Date.now()));
|
|
627
803
|
}
|
|
804
|
+
case "artifactFile": {
|
|
805
|
+
const artifact = (await this.#listArtifacts(loc.slug)).find((candidate) => candidate.name === loc.filename);
|
|
806
|
+
if (!artifact)
|
|
807
|
+
throw enoent("stat", norm);
|
|
808
|
+
return this.#fileStat(new Date(artifact.uploadedAt || Date.now()), artifact.size);
|
|
809
|
+
}
|
|
810
|
+
case "repositoryPath": {
|
|
811
|
+
const entry = await this.#repositoryEntry(loc.slug, loc.repository, loc.path);
|
|
812
|
+
if (!entry)
|
|
813
|
+
throw enoent("stat", norm);
|
|
814
|
+
return entry.kind === "directory"
|
|
815
|
+
? this.#dirStat()
|
|
816
|
+
: this.#fileStat(new Date(), "size" in entry ? entry.size ?? 0 : 0);
|
|
817
|
+
}
|
|
628
818
|
case "pullFile": {
|
|
629
819
|
const pull = await this.#matchPull(loc.slug, loc.number);
|
|
630
820
|
if (!pull)
|
|
@@ -683,10 +873,13 @@ export class SforaFs {
|
|
|
683
873
|
throw enoent("scandir", norm);
|
|
684
874
|
}
|
|
685
875
|
const entries = [
|
|
876
|
+
dirent("asks.md", false),
|
|
686
877
|
dirent("board", true),
|
|
687
|
-
dirent("docs", true),
|
|
688
878
|
dirent("drafts", true),
|
|
879
|
+
dirent("library", true),
|
|
689
880
|
dirent("links.md", false),
|
|
881
|
+
dirent("plan.md", false),
|
|
882
|
+
dirent("map.md", false),
|
|
690
883
|
dirent("posts", true),
|
|
691
884
|
dirent("pulls", true),
|
|
692
885
|
];
|
|
@@ -696,6 +889,14 @@ export class SforaFs {
|
|
|
696
889
|
}
|
|
697
890
|
return entries;
|
|
698
891
|
}
|
|
892
|
+
case "libraryDir":
|
|
893
|
+
if (!(await this.#projectExists(loc.slug)))
|
|
894
|
+
throw enoent("scandir", norm);
|
|
895
|
+
return [
|
|
896
|
+
dirent("documents", true),
|
|
897
|
+
dirent("files", true),
|
|
898
|
+
dirent("repositories", true),
|
|
899
|
+
];
|
|
699
900
|
case "postsDir": {
|
|
700
901
|
const entries = await this.#listEntries(loc.slug, loc.dir);
|
|
701
902
|
return entries.map((e) => dirent(e.filename, false));
|
|
@@ -707,6 +908,35 @@ export class SforaFs {
|
|
|
707
908
|
const notes = await this.#listNotes(loc.slug);
|
|
708
909
|
return notes.map((n) => dirent(n.filename, false));
|
|
709
910
|
}
|
|
911
|
+
case "filesDir": {
|
|
912
|
+
if (!(await this.#projectExists(loc.slug)))
|
|
913
|
+
throw enoent("scandir", norm);
|
|
914
|
+
return (await this.#listArtifacts(loc.slug)).map((artifact) => dirent(artifact.name, false));
|
|
915
|
+
}
|
|
916
|
+
case "repositoriesDir": {
|
|
917
|
+
if (!(await this.#projectExists(loc.slug)))
|
|
918
|
+
throw enoent("scandir", norm);
|
|
919
|
+
return (await this.#listRepositories(loc.slug)).map((repository) => dirent(repository.dirname, true));
|
|
920
|
+
}
|
|
921
|
+
case "repositoryPath": {
|
|
922
|
+
const current = await this.#repositoryEntry(loc.slug, loc.repository, loc.path);
|
|
923
|
+
if (!current)
|
|
924
|
+
throw enoent("scandir", norm);
|
|
925
|
+
if (current.kind !== "directory")
|
|
926
|
+
throw enotdir("scandir", norm);
|
|
927
|
+
const prefix = loc.path ? `${loc.path}/` : "";
|
|
928
|
+
const children = new Map();
|
|
929
|
+
for (const entry of (await this.#repositoryTree(loc.slug, loc.repository)).entries) {
|
|
930
|
+
if (!entry.path.startsWith(prefix) || entry.path === loc.path)
|
|
931
|
+
continue;
|
|
932
|
+
const rest = entry.path.slice(prefix.length);
|
|
933
|
+
const [name, ...tail] = rest.split("/");
|
|
934
|
+
if (!name)
|
|
935
|
+
continue;
|
|
936
|
+
children.set(name, tail.length > 0 || entry.kind === "directory");
|
|
937
|
+
}
|
|
938
|
+
return [...children].map(([name, isDir]) => dirent(name, isDir));
|
|
939
|
+
}
|
|
710
940
|
case "pullsDir": {
|
|
711
941
|
if (!(await this.#projectExists(loc.slug))) {
|
|
712
942
|
throw enoent("scandir", norm);
|
|
@@ -743,9 +973,12 @@ export class SforaFs {
|
|
|
743
973
|
case "meFile":
|
|
744
974
|
case "postFile":
|
|
745
975
|
case "docFile":
|
|
976
|
+
case "artifactFile":
|
|
746
977
|
case "pullFile":
|
|
747
978
|
case "boardCardFile":
|
|
748
979
|
case "roadmapFile":
|
|
980
|
+
case "planFile":
|
|
981
|
+
case "asksFile":
|
|
749
982
|
throw enotdir("scandir", norm);
|
|
750
983
|
default:
|
|
751
984
|
throw enoent("scandir", norm);
|
|
@@ -772,6 +1005,9 @@ export class SforaFs {
|
|
|
772
1005
|
case "projectDir":
|
|
773
1006
|
case "postsDir":
|
|
774
1007
|
case "docsDir":
|
|
1008
|
+
case "libraryDir":
|
|
1009
|
+
case "filesDir":
|
|
1010
|
+
case "repositoriesDir":
|
|
775
1011
|
case "pullsDir":
|
|
776
1012
|
case "boardDir":
|
|
777
1013
|
case "publicDir":
|
|
@@ -858,12 +1094,32 @@ export class SforaFs {
|
|
|
858
1094
|
}
|
|
859
1095
|
case "inboxFile":
|
|
860
1096
|
case "meFile":
|
|
861
|
-
case "
|
|
1097
|
+
case "artifactFile":
|
|
1098
|
+
case "repositoryPath":
|
|
862
1099
|
case "pullFile":
|
|
863
1100
|
case "roadmapFile":
|
|
864
|
-
|
|
865
|
-
|
|
1101
|
+
case "planFile":
|
|
1102
|
+
case "asksFile":
|
|
1103
|
+
// Uploaded files and repositories are read-only projections; the
|
|
1104
|
+
// roadmap and pulls are owned by their source systems; the plan and
|
|
1105
|
+
// asks are generated views that can't be unlinked.
|
|
866
1106
|
throw eacces("unlink", norm);
|
|
1107
|
+
case "docFile": {
|
|
1108
|
+
const note = await this.#matchNote(loc.slug, loc.filename);
|
|
1109
|
+
if (!note) {
|
|
1110
|
+
if (options?.force)
|
|
1111
|
+
return;
|
|
1112
|
+
throw enoent("unlink", norm);
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
await this.#client.deleteNote(loc.slug, loc.filename);
|
|
1116
|
+
}
|
|
1117
|
+
catch (e) {
|
|
1118
|
+
throw fromApi(e, "unlink", norm);
|
|
1119
|
+
}
|
|
1120
|
+
this.#invalidateNotes(loc.slug);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
867
1123
|
case "root":
|
|
868
1124
|
case "projectsDir":
|
|
869
1125
|
case "inboxDir":
|
|
@@ -871,6 +1127,9 @@ export class SforaFs {
|
|
|
871
1127
|
case "projectDir":
|
|
872
1128
|
case "postsDir":
|
|
873
1129
|
case "docsDir":
|
|
1130
|
+
case "libraryDir":
|
|
1131
|
+
case "filesDir":
|
|
1132
|
+
case "repositoriesDir":
|
|
874
1133
|
case "pullsDir":
|
|
875
1134
|
case "boardDir":
|
|
876
1135
|
case "publicDir":
|
|
@@ -982,9 +1241,16 @@ export class SforaFs {
|
|
|
982
1241
|
]);
|
|
983
1242
|
for (const slug of this.#projectSlugs) {
|
|
984
1243
|
paths.add(`/projects/${slug}`);
|
|
1244
|
+
paths.add(`/projects/${slug}/plan.md`);
|
|
1245
|
+
paths.add(`/projects/${slug}/asks.md`);
|
|
985
1246
|
paths.add(`/projects/${slug}/posts`);
|
|
986
1247
|
paths.add(`/projects/${slug}/drafts`);
|
|
987
1248
|
paths.add(`/projects/${slug}/docs`);
|
|
1249
|
+
paths.add(`/projects/${slug}/artifacts`);
|
|
1250
|
+
paths.add(`/projects/${slug}/library`);
|
|
1251
|
+
paths.add(`/projects/${slug}/library/documents`);
|
|
1252
|
+
paths.add(`/projects/${slug}/library/files`);
|
|
1253
|
+
paths.add(`/projects/${slug}/library/repositories`);
|
|
988
1254
|
paths.add(`/projects/${slug}/pulls`);
|
|
989
1255
|
paths.add(`/projects/${slug}/board`);
|
|
990
1256
|
// `public/` is only real for boards known to be shared.
|
package/dist/api-client.d.ts
CHANGED
|
@@ -38,6 +38,35 @@ export interface Note {
|
|
|
38
38
|
title: string;
|
|
39
39
|
lastEditedAt: number;
|
|
40
40
|
}
|
|
41
|
+
/** An uploaded project file under the unified Library. */
|
|
42
|
+
export interface Artifact {
|
|
43
|
+
name: string;
|
|
44
|
+
mimeType: string;
|
|
45
|
+
size: number;
|
|
46
|
+
uploadedBy: string | null;
|
|
47
|
+
uploadedAt: number;
|
|
48
|
+
url: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** A GitHub repository attached to a project. Repository content is read-only. */
|
|
51
|
+
export interface Repository {
|
|
52
|
+
projectRepoId: string;
|
|
53
|
+
dirname: string;
|
|
54
|
+
owner: string;
|
|
55
|
+
repo: string;
|
|
56
|
+
defaultBranch: string;
|
|
57
|
+
}
|
|
58
|
+
export interface RepositoryTreeEntry {
|
|
59
|
+
path: string;
|
|
60
|
+
kind: "file" | "directory" | "submodule";
|
|
61
|
+
size?: number;
|
|
62
|
+
}
|
|
63
|
+
export interface RepositoryTree {
|
|
64
|
+
owner: string;
|
|
65
|
+
repo: string;
|
|
66
|
+
ref: string;
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
entries: RepositoryTreeEntry[];
|
|
69
|
+
}
|
|
41
70
|
/**
|
|
42
71
|
* A pull request entry. Mirrors a row of `GET …/pulls`. Read-only — the source
|
|
43
72
|
* of truth is GitHub; sfora syncs these so agents can see the code work
|
|
@@ -121,6 +150,16 @@ export declare class SforaApiClient {
|
|
|
121
150
|
getProjectLinks(slug: string): Promise<string>;
|
|
122
151
|
/** `PUT …/links.md` — replace the project's links from a markdown list. */
|
|
123
152
|
setProjectLinks(slug: string, markdown: string): Promise<void>;
|
|
153
|
+
/** `GET …/plan.md` — the project's plan (goal + question buckets). */
|
|
154
|
+
readMap(slug: string): Promise<string>;
|
|
155
|
+
readPlan(slug: string): Promise<string>;
|
|
156
|
+
/**
|
|
157
|
+
* `PUT …/plan.md` — set the goal. Only the `## the goal` section is
|
|
158
|
+
* honored; the server names everything it ignored in `ignoredSections`.
|
|
159
|
+
*/
|
|
160
|
+
writePlan(slug: string, markdown: string): Promise<void>;
|
|
161
|
+
/** `GET …/asks.md` — coordination asks (read-only projection). */
|
|
162
|
+
readAsks(slug: string): Promise<string>;
|
|
124
163
|
/**
|
|
125
164
|
* `GET …/posts` or `…/drafts`. `scheduled` lists drafts that have a
|
|
126
165
|
* (future) `scheduledFor` set.
|
|
@@ -128,7 +167,7 @@ export declare class SforaApiClient {
|
|
|
128
167
|
listPosts(projectSlug: string, kind?: PostKind): Promise<Entry[]>;
|
|
129
168
|
/** `GET …/posts/:filename.md` (or `…/drafts/…`). Returns the raw markdown body. */
|
|
130
169
|
readPost(projectSlug: string, filename: string, kind?: PostKind): Promise<string>;
|
|
131
|
-
/**
|
|
170
|
+
/** Create a post or upsert a mutable draft from a Markdown file. */
|
|
132
171
|
writePost(projectSlug: string, kind: PostKind, filename: string, markdown: string): Promise<WriteResult>;
|
|
133
172
|
/** `DELETE …/(posts|drafts)/:filename.md` — soft-deletes the matched post. */
|
|
134
173
|
deletePost(projectSlug: string, kind: PostKind, filename: string): Promise<void>;
|
|
@@ -136,6 +175,13 @@ export declare class SforaApiClient {
|
|
|
136
175
|
listNotes(projectSlug: string): Promise<Note[]>;
|
|
137
176
|
/** `GET …/docs/:filename.md`. Returns the raw markdown body. */
|
|
138
177
|
readNote(projectSlug: string, filename: string): Promise<string>;
|
|
178
|
+
writeNote(projectSlug: string, filename: string, markdown: string): Promise<WriteResult>;
|
|
179
|
+
deleteNote(projectSlug: string, filename: string): Promise<void>;
|
|
180
|
+
listArtifacts(projectSlug: string): Promise<Artifact[]>;
|
|
181
|
+
readArtifact(projectSlug: string, filename: string): Promise<string>;
|
|
182
|
+
listRepositories(projectSlug: string): Promise<Repository[]>;
|
|
183
|
+
getRepositoryTree(projectSlug: string, dirname: string): Promise<RepositoryTree>;
|
|
184
|
+
readRepositoryFile(projectSlug: string, dirname: string, path: string): Promise<string>;
|
|
139
185
|
/** `GET …/pulls` — the project's synced pull requests, open first. */
|
|
140
186
|
listPulls(projectSlug: string): Promise<Pull[]>;
|
|
141
187
|
/**
|
package/dist/api-client.js
CHANGED
|
@@ -102,6 +102,25 @@ export class SforaApiClient {
|
|
|
102
102
|
async setProjectLinks(slug, markdown) {
|
|
103
103
|
await this.#request("PUT", `/v1/fs/projects/${encodeURIComponent(slug)}/links.md`, markdown);
|
|
104
104
|
}
|
|
105
|
+
/** `GET …/plan.md` — the project's plan (goal + question buckets). */
|
|
106
|
+
// The map as a file: derived, read-only (a PUT is refused server-side).
|
|
107
|
+
async readMap(slug) {
|
|
108
|
+
return this.#text(`/v1/fs/projects/${encodeURIComponent(slug)}/map.md`);
|
|
109
|
+
}
|
|
110
|
+
async readPlan(slug) {
|
|
111
|
+
return this.#text(`/v1/fs/projects/${encodeURIComponent(slug)}/plan.md`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `PUT …/plan.md` — set the goal. Only the `## the goal` section is
|
|
115
|
+
* honored; the server names everything it ignored in `ignoredSections`.
|
|
116
|
+
*/
|
|
117
|
+
async writePlan(slug, markdown) {
|
|
118
|
+
await this.#request("PUT", `/v1/fs/projects/${encodeURIComponent(slug)}/plan.md`, markdown);
|
|
119
|
+
}
|
|
120
|
+
/** `GET …/asks.md` — coordination asks (read-only projection). */
|
|
121
|
+
async readAsks(slug) {
|
|
122
|
+
return this.#text(`/v1/fs/projects/${encodeURIComponent(slug)}/asks.md`);
|
|
123
|
+
}
|
|
105
124
|
/**
|
|
106
125
|
* `GET …/posts` or `…/drafts`. `scheduled` lists drafts that have a
|
|
107
126
|
* (future) `scheduledFor` set.
|
|
@@ -123,7 +142,7 @@ export class SforaApiClient {
|
|
|
123
142
|
const file = encodeURIComponent(filename);
|
|
124
143
|
return this.#text(`/v1/fs/projects/${slug}/${base}/${file}`);
|
|
125
144
|
}
|
|
126
|
-
/**
|
|
145
|
+
/** Create a post or upsert a mutable draft from a Markdown file. */
|
|
127
146
|
async writePost(projectSlug, kind, filename, markdown) {
|
|
128
147
|
const base = routeBase(kind);
|
|
129
148
|
const slug = encodeURIComponent(projectSlug);
|
|
@@ -143,14 +162,52 @@ export class SforaApiClient {
|
|
|
143
162
|
/** `GET …/docs` — the project's notes, most-recently-edited first. */
|
|
144
163
|
async listNotes(projectSlug) {
|
|
145
164
|
const slug = encodeURIComponent(projectSlug);
|
|
146
|
-
const data = await this.#json(`/v1/fs/projects/${slug}/
|
|
165
|
+
const data = await this.#json(`/v1/fs/projects/${slug}/library/documents`);
|
|
147
166
|
return data.docs ?? [];
|
|
148
167
|
}
|
|
149
168
|
/** `GET …/docs/:filename.md`. Returns the raw markdown body. */
|
|
150
169
|
async readNote(projectSlug, filename) {
|
|
151
170
|
const slug = encodeURIComponent(projectSlug);
|
|
152
171
|
const file = encodeURIComponent(filename);
|
|
153
|
-
return this.#text(`/v1/fs/projects/${slug}/
|
|
172
|
+
return this.#text(`/v1/fs/projects/${slug}/library/documents/${file}`);
|
|
173
|
+
}
|
|
174
|
+
async writeNote(projectSlug, filename, markdown) {
|
|
175
|
+
const slug = encodeURIComponent(projectSlug);
|
|
176
|
+
const file = encodeURIComponent(filename);
|
|
177
|
+
const res = await this.#request("PUT", `/v1/fs/projects/${slug}/library/documents/${file}`, markdown);
|
|
178
|
+
return (await res.json());
|
|
179
|
+
}
|
|
180
|
+
async deleteNote(projectSlug, filename) {
|
|
181
|
+
const slug = encodeURIComponent(projectSlug);
|
|
182
|
+
const file = encodeURIComponent(filename);
|
|
183
|
+
await this.#request("DELETE", `/v1/fs/projects/${slug}/library/documents/${file}`);
|
|
184
|
+
}
|
|
185
|
+
// ─── Unified project Library ────────────────────────────────────
|
|
186
|
+
async listArtifacts(projectSlug) {
|
|
187
|
+
const slug = encodeURIComponent(projectSlug);
|
|
188
|
+
const data = await this.#json(`/v1/fs/projects/${slug}/library/files`);
|
|
189
|
+
return data.artifacts ?? [];
|
|
190
|
+
}
|
|
191
|
+
async readArtifact(projectSlug, filename) {
|
|
192
|
+
const slug = encodeURIComponent(projectSlug);
|
|
193
|
+
const file = encodeURIComponent(filename);
|
|
194
|
+
return this.#text(`/v1/fs/projects/${slug}/library/files/${file}`);
|
|
195
|
+
}
|
|
196
|
+
async listRepositories(projectSlug) {
|
|
197
|
+
const slug = encodeURIComponent(projectSlug);
|
|
198
|
+
const data = await this.#json(`/v1/fs/projects/${slug}/library/repositories`);
|
|
199
|
+
return data.repositories ?? [];
|
|
200
|
+
}
|
|
201
|
+
async getRepositoryTree(projectSlug, dirname) {
|
|
202
|
+
const slug = encodeURIComponent(projectSlug);
|
|
203
|
+
const repo = encodeURIComponent(dirname);
|
|
204
|
+
return this.#json(`/v1/fs/projects/${slug}/library/repositories/${repo}`);
|
|
205
|
+
}
|
|
206
|
+
async readRepositoryFile(projectSlug, dirname, path) {
|
|
207
|
+
const slug = encodeURIComponent(projectSlug);
|
|
208
|
+
const repo = encodeURIComponent(dirname);
|
|
209
|
+
const file = path.split("/").map(encodeURIComponent).join("/");
|
|
210
|
+
return this.#text(`/v1/fs/projects/${slug}/library/repositories/${repo}/${file}`);
|
|
154
211
|
}
|
|
155
212
|
// ─── Pull requests (read-only) ───────────────────────────────────
|
|
156
213
|
/** `GET …/pulls` — the project's synced pull requests, open first. */
|