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.
Files changed (71) hide show
  1. package/README.md +8 -6
  2. package/dist/SforaFs.js +270 -4
  3. package/dist/api-client.d.ts +47 -1
  4. package/dist/api-client.js +60 -3
  5. package/dist/cli.js +24 -11
  6. package/dist/format/__tests__/byteStable.d.ts +5 -0
  7. package/dist/format/__tests__/byteStable.js +64 -0
  8. package/dist/format/blocks/dropClosure.d.ts +72 -0
  9. package/dist/format/blocks/dropClosure.js +186 -0
  10. package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
  11. package/dist/format/blocks/markdown-block-catalog.js +162 -0
  12. package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
  13. package/dist/format/blocks/markdown-block-ids.mjs +25 -0
  14. package/dist/format/blocks/parsers.d.ts +105 -0
  15. package/dist/format/blocks/parsers.js +442 -0
  16. package/dist/format/blocks/structured-block-schema.d.ts +8 -0
  17. package/dist/format/blocks/structured-block-schema.js +30 -0
  18. package/dist/format/callout.d.ts +66 -0
  19. package/dist/format/callout.js +130 -0
  20. package/dist/format/cardMarkdown.d.ts +4 -0
  21. package/dist/format/cardMarkdown.js +12 -0
  22. package/dist/format/checklist.d.ts +34 -0
  23. package/dist/format/checklist.js +151 -0
  24. package/dist/format/index.d.ts +18 -4
  25. package/dist/format/index.js +24 -4
  26. package/dist/format/lineGeometry.d.ts +70 -0
  27. package/dist/format/lineGeometry.js +324 -0
  28. package/dist/format/lint/index.d.ts +20 -0
  29. package/dist/format/lint/index.js +22 -0
  30. package/dist/format/lint/lintSource.d.ts +36 -0
  31. package/dist/format/lint/lintSource.js +154 -0
  32. package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
  33. package/dist/format/lint/rules/broken-wiki-link.js +45 -0
  34. package/dist/format/lint/rules/index.d.ts +10 -0
  35. package/dist/format/lint/rules/index.js +26 -0
  36. package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
  37. package/dist/format/lint/rules/malformed-callout.js +79 -0
  38. package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
  39. package/dist/format/lint/rules/malformed-checklist.js +60 -0
  40. package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
  41. package/dist/format/lint/rules/malformed-frontmatter.js +93 -0
  42. package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
  43. package/dist/format/lint/rules/malformed-structured-block.js +134 -0
  44. package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
  45. package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
  46. package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
  47. package/dist/format/lint/rules/orphan-reference.js +87 -0
  48. package/dist/format/lint/types.d.ts +80 -0
  49. package/dist/format/lint/types.js +16 -0
  50. package/dist/format/markdown/dates.js +2 -0
  51. package/dist/format/markdown/document.js +2 -0
  52. package/dist/format/markdown/index.js +2 -0
  53. package/dist/format/markdown/mentions.js +2 -0
  54. package/dist/format/markdown/slug.js +2 -0
  55. package/dist/format/markdown/yaml.js +2 -0
  56. package/dist/format/noteMarkdown.js +2 -0
  57. package/dist/format/parseWithFallback.d.ts +13 -0
  58. package/dist/format/parseWithFallback.js +98 -0
  59. package/dist/format/plaintext.d.ts +5 -0
  60. package/dist/format/plaintext.js +41 -0
  61. package/dist/format/postMarkdown.js +3 -1
  62. package/dist/format/taskUploadFilename.d.ts +6 -0
  63. package/dist/format/taskUploadFilename.js +13 -0
  64. package/dist/format/wayfinder.d.ts +50 -0
  65. package/dist/format/wayfinder.js +203 -0
  66. package/dist/format/wikiLinks.d.ts +19 -0
  67. package/dist/format/wikiLinks.js +80 -0
  68. package/dist/local/workspace.d.ts +12 -0
  69. package/dist/local/workspace.js +100 -7
  70. package/dist/mcp-server.js +11 -5
  71. package/package.json +7 -6
@@ -19,20 +19,111 @@ import { join, dirname, resolve } from "node:path";
19
19
  import { parseMarkdownCard, cardFilename, numberFromFilename, columnSlugFromDirname, parseMarkdownPost, parseMarkdownNote, noteFilename, slugify, } from "../format/index.js";
20
20
  /** Directory name that marks a local sfora workspace. */
21
21
  export const WORKSPACE_DIR = ".sfora";
22
- const DEFAULT_COLUMNS = ["01-todo", "02-in-progress", "03-done"];
22
+ // One Flow: every board is the same four fixed stage columns, matching the cloud
23
+ // byte-for-byte, so `cp` stays a migration. There is no column management.
24
+ const DEFAULT_COLUMNS = [
25
+ "01-triage",
26
+ "02-todo",
27
+ "03-in-progress",
28
+ "04-done",
29
+ ];
30
+ // Map a legacy column slug → its stage dir. Mirrors the server's migrateStages
31
+ // name-mapping; anything unrecognized falls to "02-todo" (with the old name kept
32
+ // as a label so the lane isn't lost).
33
+ const STAGE_DIR_BY_SLUG = {
34
+ triage: "01-triage",
35
+ undecided: "01-triage",
36
+ someday: "01-triage",
37
+ icebox: "01-triage",
38
+ todo: "02-todo",
39
+ "to-do": "02-todo",
40
+ "in-progress": "03-in-progress",
41
+ doing: "03-in-progress",
42
+ wip: "03-in-progress",
43
+ working: "03-in-progress",
44
+ done: "04-done",
45
+ complete: "04-done",
46
+ completed: "04-done",
47
+ shipped: "04-done",
48
+ };
23
49
  const WORKSPACE_README = `# sfora workspace
24
50
 
25
51
  Everything here is a plain markdown file — edit with any tool, version with git.
26
52
 
27
- - \`board/<column>/NNNN-<slug>.md\` — tasks. Frontmatter: \`status\`, \`priority\`,
28
- \`labels\`, \`assignees\`, \`due\`. Move a task by moving the file between column
29
- directories (\`mv\` works).
53
+ - \`board/<column>/NNNN-<slug>.md\` — tasks. Every board is the same four fixed
54
+ columns — \`01-triage / 02-todo / 03-in-progress / 04-done\`. Move a task by
55
+ moving the file between column dirs (\`mv\` works); moving into \`04-done\`
56
+ marks it done. Frontmatter: \`status\`, \`priority\`, \`labels\`, \`assignees\`, \`due\`.
30
57
  - \`posts/YYYY-MM-DD-<slug>.md\` — posts. An H1 (\`# Title\`) is the title.
31
58
  - \`docs/<slug>.md\` — docs.
32
59
 
33
60
  Create files by hand, or use the CLI: \`sfora task plan.md\`, \`sfora post note.md\`.
34
61
  Same format as sfora cloud — \`sfora login\` connects this workspace to a team.
35
62
  `;
63
+ // Surgically add a label to a card's YAML frontmatter without reformatting the
64
+ // rest of the file (local files are hand-edited; we don't round-trip them).
65
+ function addLabelToCard(md, label) {
66
+ const fm = /^---\n([\s\S]*?)\n---\n?/.exec(md);
67
+ if (!fm)
68
+ return `---\nlabels: [${label}]\n---\n\n${md}`;
69
+ const block = fm[1];
70
+ const labelLine = /^labels:\s*\[(.*)\]\s*$/m.exec(block);
71
+ if (labelLine) {
72
+ const items = labelLine[1]
73
+ .split(",")
74
+ .map((s) => s.trim())
75
+ .filter(Boolean);
76
+ if (items.includes(label))
77
+ return md;
78
+ const replaced = block.replace(labelLine[0], `labels: [${[...items, label].join(", ")}]`);
79
+ return md.replace(block, replaced);
80
+ }
81
+ return md.replace(block, `${block}\nlabels: [${label}]`);
82
+ }
83
+ /**
84
+ * Migrate an existing workspace's board onto the four fixed stage dirs (One
85
+ * Flow). Idempotent: a board already in the canonical shape is left untouched.
86
+ * Legacy columns are mapped by name (done→done, in progress→doing, triage/
87
+ * undecided/…→triage, else todo); each card file MOVES into its stage dir, and
88
+ * a card leaving an unrecognized column keeps that column's name as a label so
89
+ * nothing is lost. Called on open so old \`.sfora/\` dirs reshape silently.
90
+ */
91
+ export async function migrateWorkspaceStages(root) {
92
+ const boardDir = join(root, "board");
93
+ const entries = await readdir(boardDir, { withFileTypes: true }).catch(() => []);
94
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
95
+ if (dirs.length === 0)
96
+ return { migrated: false, moved: 0 };
97
+ const isCanonical = dirs.length === DEFAULT_COLUMNS.length &&
98
+ DEFAULT_COLUMNS.every((d) => dirs.includes(d));
99
+ if (isCanonical)
100
+ return { migrated: false, moved: 0 };
101
+ for (const d of DEFAULT_COLUMNS) {
102
+ await mkdir(join(boardDir, d), { recursive: true });
103
+ }
104
+ let moved = 0;
105
+ for (const oldDir of dirs) {
106
+ if (DEFAULT_COLUMNS.includes(oldDir))
107
+ continue; // canonical dir stays put
108
+ const slug = columnSlugFromDirname(oldDir);
109
+ const canonical = STAGE_DIR_BY_SLUG[slug];
110
+ const target = canonical ?? "02-todo";
111
+ const files = await readdir(join(boardDir, oldDir)).catch(() => []);
112
+ for (const f of files) {
113
+ if (!f.endsWith(".md"))
114
+ continue;
115
+ const src = join(boardDir, oldDir, f);
116
+ let md = await readFile(src, "utf8");
117
+ if (!canonical)
118
+ md = addLabelToCard(md, slug); // preserve the old lane
119
+ await writeFile(join(boardDir, target, f), md, "utf8");
120
+ await rm(src);
121
+ moved++;
122
+ }
123
+ await rm(join(boardDir, oldDir), { recursive: true, force: true }).catch(() => { });
124
+ }
125
+ return { migrated: true, moved };
126
+ }
36
127
  /**
37
128
  * Walk up from `cwd` looking for a `.sfora/` workspace. A directory only
38
129
  * counts when it has workspace markers (board/posts/docs) — `~/.sfora` is also
@@ -194,8 +285,8 @@ export class LocalWorkspace {
194
285
  // ─── Internals ───────────────────────────────────────────────────
195
286
  /**
196
287
  * Resolve a column reference (slug or name, e.g. "todo" / "In progress") to
197
- * an existing column dirname. With no reference: a closed status prefers a
198
- * done-ish column, otherwise the first column.
288
+ * an existing column dirname. With no reference: a closed status prefers the
289
+ * done column, otherwise "To do" (new work is up for grabs, not in triage).
199
290
  */
200
291
  async #resolveColumn(ref, status) {
201
292
  const columns = await this.listColumns();
@@ -217,7 +308,9 @@ export class LocalWorkspace {
217
308
  if (done)
218
309
  return done;
219
310
  }
220
- return columns[0];
311
+ // Default target is "To do" (02-todo), not the leading triage column.
312
+ const todo = columns.find((c) => columnSlugFromDirname(c) === "todo");
313
+ return todo ?? columns[0];
221
314
  }
222
315
  async #nextNumber() {
223
316
  let max = 0;
@@ -13,18 +13,24 @@ import { createSforaShell, createLocalShell } from "./index.js";
13
13
  const TOOL_DESCRIPTION = `Run a bash command against the sfora workspace — a Unix-style view where every post, task, and doc is a markdown file:
14
14
  - /projects/<slug>/posts/<file>.md published posts
15
15
  - /projects/<slug>/drafts/<file>.md your drafts
16
- - /projects/<slug>/board/<NN-col>/<NNNN>.md tasks (kanban cards), by column
17
- - /projects/<slug>/docs/<file>.md docs / notes
16
+ - /projects/<slug>/board/<NN-stage>/<NNNN>.md tasks (kanban cards), by stage
17
+ - /projects/<slug>/library/documents/<file>.md workspace documents (writable)
18
+ - /projects/<slug>/library/files/<file> uploaded files (read-only)
19
+ - /projects/<slug>/library/repositories/<repo> project source trees (read-only)
18
20
  - /projects/<slug>/pulls/<number>.md pull requests (diff + linked work), read-only
21
+ - /projects/<slug>/plan.md the goal + open questions (write to set the goal)
22
+ - /projects/<slug>/asks.md coordination asks, read-only
19
23
  - /inbox/mentions.md unread mentions
20
24
  - /me/api-key your identity
21
- Examples: 'ls /projects', 'cat /projects/web/board/01-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/01-todo/fix.md'.
25
+ Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; move a card into 04-done to close it. Question cards (kind: question) map their stage to the plan: triage = fuzzy, todo = up for grabs, in-progress = claimed, done = decided.
26
+ Examples: 'ls /projects', 'cat /projects/web/board/02-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/02-todo/fix.md', 'mv /projects/web/board/02-todo/0003-*.md /projects/web/board/04-done/'.
22
27
  Write a file to create or update the entity (frontmatter sets fields like status/priority/assignees/due). cwd and environment persist across calls.`;
23
28
  const LOCAL_TOOL_DESCRIPTION = `Run a bash command against the local sfora workspace (a .sfora/ directory of plain markdown files, git-versioned with the repo):
24
- - /board/<NN-col>/<NNNN>-<slug>.md tasks (kanban cards), by column — 'mv' between column dirs moves a task
29
+ - /board/<NN-stage>/<NNNN>-<slug>.md tasks (kanban cards), by stage — 'mv' between stage dirs moves a task
25
30
  - /posts/<YYYY-MM-DD>-<slug>.md posts
26
31
  - /docs/<slug>.md docs / notes
27
- Examples: 'ls /board/01-todo', 'cat /board/01-todo/*.md', 'grep -ri TODO /', 'echo "# Fix login\\nstatus: active" > /board/01-todo/fix-login.md', 'mv /board/01-todo/0003-*.md /board/03-done/'.
32
+ Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; moving a card into 04-done marks it done.
33
+ Examples: 'ls /board/02-todo', 'cat /board/02-todo/*.md', 'grep -ri TODO /', 'echo "# Fix login\\nstatus: active" > /board/02-todo/fix-login.md', 'mv /board/02-todo/0003-*.md /board/04-done/'.
28
34
  Frontmatter sets task fields (status/priority/labels/assignees/due). cwd and environment persist across calls.`;
29
35
  export async function runMcpServer(options) {
30
36
  const { bash } = options.localRoot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfora-cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Your sfora workspace as a markdown filesystem — a CLI + MCP server. Post/task/doc, ls/cat/grep, and a shell so agents operate sfora natively.",
6
6
  "keywords": [
@@ -36,15 +36,16 @@
36
36
  "README.md"
37
37
  ],
38
38
  "scripts": {
39
- "build": "tsc -p .",
40
- "dev": "tsx src/cli.ts",
41
- "typecheck": "tsc --noEmit"
39
+ "build": "node scripts/sync-format.mjs && tsc -p .",
40
+ "dev": "node scripts/sync-format.mjs && tsx src/cli.ts",
41
+ "typecheck": "node scripts/sync-format.mjs && tsc --noEmit"
42
42
  },
43
43
  "dependencies": {
44
- "just-bash": "^3.0.1",
45
- "@modelcontextprotocol/sdk": "^1.0.0"
44
+ "@modelcontextprotocol/sdk": "^1.0.0",
45
+ "just-bash": "^3.0.1"
46
46
  },
47
47
  "devDependencies": {
48
+ "@types/mdast": "^4.0.4",
48
49
  "@types/node": "^22.10.0",
49
50
  "tsx": "^4.20.3",
50
51
  "typescript": "^5.9.3"