sfora-cli 0.9.0 → 0.11.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 +147 -6
- package/dist/SforaFs.js +278 -10
- package/dist/api-client.d.ts +290 -5
- package/dist/api-client.js +307 -22
- package/dist/block-commands.d.ts +84 -0
- package/dist/block-commands.js +155 -0
- package/dist/cli.js +323 -29
- package/dist/format/__tests__/byteStable.d.ts +5 -0
- package/dist/format/__tests__/byteStable.js +64 -0
- package/dist/format/blockSplice.d.ts +135 -0
- package/dist/format/blockSplice.js +330 -0
- package/dist/format/blocks/dropClosure.d.ts +81 -0
- package/dist/format/blocks/dropClosure.js +196 -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 +128 -0
- package/dist/format/callout.js +227 -0
- package/dist/format/cardMarkdown.d.ts +2 -0
- package/dist/format/cardMarkdown.js +10 -0
- package/dist/format/checklist.d.ts +34 -0
- package/dist/format/checklist.js +158 -0
- package/dist/format/formatAxes.d.ts +228 -0
- package/dist/format/formatAxes.js +454 -0
- package/dist/format/index.d.ts +19 -4
- package/dist/format/index.js +28 -4
- package/dist/format/lineGeometry.d.ts +100 -0
- package/dist/format/lineGeometry.js +424 -0
- package/dist/format/lint/appliesTo.d.ts +92 -0
- package/dist/format/lint/appliesTo.js +369 -0
- package/dist/format/lint/config.d.ts +106 -0
- package/dist/format/lint/config.js +205 -0
- package/dist/format/lint/fixAll.d.ts +62 -0
- package/dist/format/lint/fixAll.js +107 -0
- package/dist/format/lint/frontmatterSchema.d.ts +181 -0
- package/dist/format/lint/frontmatterSchema.js +660 -0
- package/dist/format/lint/index.d.ts +49 -0
- package/dist/format/lint/index.js +51 -0
- package/dist/format/lint/lintSource.d.ts +56 -0
- package/dist/format/lint/lintSource.js +188 -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/frontmatter-schema.d.ts +2 -0
- package/dist/format/lint/rules/frontmatter-schema.js +92 -0
- package/dist/format/lint/rules/index.d.ts +11 -0
- package/dist/format/lint/rules/index.js +32 -0
- package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
- package/dist/format/lint/rules/malformed-callout.js +88 -0
- package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
- package/dist/format/lint/rules/malformed-checklist.js +65 -0
- package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
- package/dist/format/lint/rules/malformed-frontmatter.js +98 -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/severity.d.ts +15 -0
- package/dist/format/lint/severity.js +50 -0
- package/dist/format/lint/textEdits.d.ts +86 -0
- package/dist/format/lint/textEdits.js +162 -0
- package/dist/format/lint/types.d.ts +116 -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.d.ts +28 -0
- package/dist/format/markdown/slug.js +65 -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 +51 -0
- package/dist/format/postMarkdown.js +3 -1
- package/dist/format/sheetCellSpans.d.ts +95 -0
- package/dist/format/sheetCellSpans.js +223 -0
- package/dist/format/sheetSelection.d.ts +136 -0
- package/dist/format/sheetSelection.js +282 -0
- package/dist/format/taskUploadFilename.d.ts +6 -0
- package/dist/format/taskUploadFilename.js +13 -0
- package/dist/format/textStats.d.ts +23 -0
- package/dist/format/textStats.js +80 -0
- package/dist/format/wayfinder.d.ts +50 -0
- package/dist/format/wayfinder.js +203 -0
- package/dist/format/wikiLinks.d.ts +78 -0
- package/dist/format/wikiLinks.js +266 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +20 -3
- package/dist/mcp-server.js +5 -2
- package/dist/opener.d.ts +23 -0
- package/dist/opener.js +26 -0
- package/dist/render.d.ts +132 -0
- package/dist/render.js +208 -0
- package/dist/shell-commands.d.ts +34 -0
- package/dist/shell-commands.js +108 -0
- package/dist/watch.d.ts +79 -0
- package/dist/watch.js +113 -0
- package/dist/web-url.d.ts +39 -0
- package/dist/web-url.js +63 -0
- package/package.json +7 -6
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type WikiLinkKind = "post" | "card" | "note" | "pr";
|
|
2
|
+
export declare const WIKI_LINK_PREFIXES: ReadonlyArray<readonly [prefix: string, kind: WikiLinkKind]>;
|
|
3
|
+
export declare const WIKI_LINK_PREFIX_FOR: Readonly<Record<WikiLinkKind, string>>;
|
|
4
|
+
export declare const WIKI_EMBED_MARKER = "!";
|
|
5
|
+
export declare const BLOCK_ANCHOR_MARKER = "^";
|
|
6
|
+
export declare function buildWikiLink(kind: WikiLinkKind, id: string, label: string): string;
|
|
7
|
+
export declare function buildWikiEmbed(kind: WikiLinkKind, id: string, label: string): string;
|
|
8
|
+
export declare function cleanWikiTarget(value: string): string;
|
|
9
|
+
export declare function cleanWikiLabel(value: string): string;
|
|
10
|
+
export declare function cleanWikiAnchor(value: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Split a target-with-anchor into its two halves, honouring the prefix table.
|
|
13
|
+
*
|
|
14
|
+
* The `#` is only a separator after at least one character of the BARE id, and
|
|
15
|
+
* that is not a nicety — `[[c:#42]]` is the existing "card by number" form
|
|
16
|
+
* (`parseWikiTarget` yields `{kind:"card", id:"#42"}`). Splitting on the first
|
|
17
|
+
* `#` anywhere would turn it into a link to the prefix `c:` with an anchor of
|
|
18
|
+
* `42`, silently unlinking every task referenced by number. So: find the
|
|
19
|
+
* prefix, then look for a `#` past the first character after it.
|
|
20
|
+
*
|
|
21
|
+
* An empty anchor (`[[n:abc#]]`) is NOT an anchor. The trailing `#` stays part
|
|
22
|
+
* of the target, which resolves to nothing and renders as the muted
|
|
23
|
+
* unknown-entity text the grammar already gives any unresolvable target —
|
|
24
|
+
* open-knowledge's tokenizer takes the same position from the other side, by
|
|
25
|
+
* refusing to close a wiki link on a zero-length anchor at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitWikiAnchor(target: string): {
|
|
28
|
+
target: string;
|
|
29
|
+
anchor: string | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The BLOCK id an anchor addresses, or null when the anchor names a heading.
|
|
33
|
+
*
|
|
34
|
+
* One character decides it, and the split is done here rather than at each
|
|
35
|
+
* consumer so "does this token point at a block" has one answer. Three cases
|
|
36
|
+
* are deliberate:
|
|
37
|
+
*
|
|
38
|
+
* `^k7f3a2cx` → `"k7f3a2cx"`. The ordinal form of a duplicated block,
|
|
39
|
+
* `^k7f3a2cx.2`, comes back whole — the dot is part of the id
|
|
40
|
+
* (`fingerprintBlocks`), not a fourth separator.
|
|
41
|
+
* `Rollout` → null. A heading, and the reader slugs it as it always has.
|
|
42
|
+
* `^` → null. A marker with nothing after it addresses no block, the
|
|
43
|
+
* same position `splitWikiAnchor` takes on `[[n:abc#]]`: an
|
|
44
|
+
* empty half is not a half. It stays a (meaningless) heading
|
|
45
|
+
* anchor rather than becoming a block id of `""`, because an id
|
|
46
|
+
* nothing can resolve is worse than an anchor that resolves to
|
|
47
|
+
* no heading.
|
|
48
|
+
*
|
|
49
|
+
* The cost, stated: a heading whose text genuinely begins with `^` cannot be
|
|
50
|
+
* addressed as a heading. That is inherent in a one-character prefix and it is
|
|
51
|
+
* Obsidian's trade too; the alternative — a separate segment — would have every
|
|
52
|
+
* consumer of `anchor` learn a new shape to gain a case nobody writes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function blockAnchorId(anchor: string | null | undefined): string | null;
|
|
55
|
+
/** The anchor bytes that address `blockId` — the inverse of `blockAnchorId`. */
|
|
56
|
+
export declare function blockAnchor(blockId: string): string;
|
|
57
|
+
/** Re-join a target and anchor into the bytes that go between the brackets. */
|
|
58
|
+
export declare function joinWikiAnchor(target: string, anchor: string | null | undefined): string;
|
|
59
|
+
export interface WikiLink {
|
|
60
|
+
kind: WikiLinkKind;
|
|
61
|
+
id: string;
|
|
62
|
+
target: string;
|
|
63
|
+
anchor: string | null;
|
|
64
|
+
blockId: string | null;
|
|
65
|
+
label: string;
|
|
66
|
+
hasLabel: boolean;
|
|
67
|
+
embed: boolean;
|
|
68
|
+
}
|
|
69
|
+
export declare function wikiLinkPattern(): RegExp;
|
|
70
|
+
export declare function wikiTokenPattern(): RegExp;
|
|
71
|
+
export declare function parseWikiToken(raw: string, embed?: boolean): WikiLink;
|
|
72
|
+
export declare function parseWikiTarget(target: string): {
|
|
73
|
+
kind: WikiLinkKind;
|
|
74
|
+
id: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function parseWikiLinks(body: string): WikiLink[];
|
|
77
|
+
export declare function matchWikiToken(text: string): WikiLink | null;
|
|
78
|
+
export declare function matchWikiEmbed(text: string): WikiLink | null;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
|
|
2
|
+
// Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
|
|
3
|
+
// The `[[…]]` wiki-link grammar — sfora's cross-entity link token, and the one
|
|
4
|
+
// place its prefix table lives.
|
|
5
|
+
//
|
|
6
|
+
// [[<postId>]] → a post (bare, no prefix)
|
|
7
|
+
// [[n:<noteId>]] → a doc / note
|
|
8
|
+
// [[c:<cardId>]] → a task / card (also accepts `c:#42` / `c:42`)
|
|
9
|
+
// [[pr:<n>]] / [[gh:<n>]] → a pull request
|
|
10
|
+
//
|
|
11
|
+
// Any form may carry a display label after a pipe: `[[c:42|Fix login]]`, and
|
|
12
|
+
// any form may point INTO its target at a heading, after a hash:
|
|
13
|
+
//
|
|
14
|
+
// [[n:<noteId>#<heading>]]
|
|
15
|
+
// [[n:<noteId>#<heading>|Label]]
|
|
16
|
+
//
|
|
17
|
+
// The three segments are ordered — target, then anchor, then label — which is
|
|
18
|
+
// what lets a `#` inside a label stay an ordinary character and a `|` after an
|
|
19
|
+
// anchor still open the label.
|
|
20
|
+
//
|
|
21
|
+
// The anchor has TWO readings, told apart by one character:
|
|
22
|
+
//
|
|
23
|
+
// [[n:<noteId>#Rollout]] → a heading, matched through `toAnchorSlug`
|
|
24
|
+
// [[n:<noteId>#^k7f3a2cx]] → a BLOCK, matched by its derived fingerprint
|
|
25
|
+
//
|
|
26
|
+
// The `^` is Obsidian's spelling and it is a prefix rather than a separate
|
|
27
|
+
// segment on purpose: a block id and a heading slug are the same KIND of thing
|
|
28
|
+
// — a place inside the target — so they share the anchor position, the anchor
|
|
29
|
+
// splitter, the anchor cleaner and the label rule, and no consumer has to learn
|
|
30
|
+
// a fourth segment to keep up. Card #325, against
|
|
31
|
+
// `docs/research/durable-block-ids-design.md` card 7.
|
|
32
|
+
//
|
|
33
|
+
// A leading `!` turns any of those forms into an EMBED — `![[c:42]]` — which
|
|
34
|
+
// says "show the thing here" rather than "point at it". The bang is the only
|
|
35
|
+
// difference: everything after it is the same three segments, read by the same
|
|
36
|
+
// splitters and written by the same sanitizers, so an embed can never disagree
|
|
37
|
+
// with a link about what a target or an anchor is. Card #298.
|
|
38
|
+
//
|
|
39
|
+
// Pure string transforms — no React, no Convex, no Node. Consumers: the Convex
|
|
40
|
+
// reference indexer (convex/references.ts), the web renderer
|
|
41
|
+
// (src/lib/utils/markdown.ts), and, by hand-copy, the mobile preprocessor
|
|
42
|
+
// (packages/mobile/src/lib/markdown-tokens.ts — outside the pnpm workspace).
|
|
43
|
+
// Prefix → kind, longest-first so `pr:` is tried before any future `p:`.
|
|
44
|
+
export const WIKI_LINK_PREFIXES = [
|
|
45
|
+
["pr:", "pr"],
|
|
46
|
+
["gh:", "pr"],
|
|
47
|
+
["n:", "note"],
|
|
48
|
+
["c:", "card"],
|
|
49
|
+
];
|
|
50
|
+
// The prefix to WRITE for a kind when building a canonical token. Note `gh:` is
|
|
51
|
+
// an accepted input alias for `pr:` but is never emitted.
|
|
52
|
+
export const WIKI_LINK_PREFIX_FOR = {
|
|
53
|
+
post: "",
|
|
54
|
+
card: "c:",
|
|
55
|
+
note: "n:",
|
|
56
|
+
pr: "pr:",
|
|
57
|
+
};
|
|
58
|
+
// The one byte that separates an embed from a link, named so no consumer has
|
|
59
|
+
// to spell `"!"` and mean "the embed marker".
|
|
60
|
+
export const WIKI_EMBED_MARKER = "!";
|
|
61
|
+
// The one byte that separates a BLOCK anchor from a heading anchor, named for
|
|
62
|
+
// the same reason. `#^k7f3a2cx` addresses a block by the fingerprint
|
|
63
|
+
// `@sfora/markdown/blockIds` derives from its content; `#Rollout` addresses a
|
|
64
|
+
// heading by its slug.
|
|
65
|
+
export const BLOCK_ANCHOR_MARKER = "^";
|
|
66
|
+
// Build the canonical `[[<prefix><id>|<label>]]` form. The label is sanitized:
|
|
67
|
+
// `[`, `]`, and `|` inside it would break the token grammar.
|
|
68
|
+
export function buildWikiLink(kind, id, label) {
|
|
69
|
+
const clean = label.replace(/[[\]|]/g, " ").trim() || "Untitled";
|
|
70
|
+
return `[[${WIKI_LINK_PREFIX_FOR[kind]}${id}|${clean}]]`;
|
|
71
|
+
}
|
|
72
|
+
// The same token with the embed marker in front. Built by delegating rather
|
|
73
|
+
// than by re-spelling the brackets: an embed is a link with a bang, and the day
|
|
74
|
+
// `buildWikiLink` learns a fourth segment this keeps up for free.
|
|
75
|
+
export function buildWikiEmbed(kind, id, label) {
|
|
76
|
+
return `${WIKI_EMBED_MARKER}${buildWikiLink(kind, id, label)}`;
|
|
77
|
+
}
|
|
78
|
+
// The two sanitizers that guard the token's own grammar on the WRITE side, and
|
|
79
|
+
// the one place they live. Both halves of `[[target|label]]` pass through one
|
|
80
|
+
// of them, and the two halves are not treated alike:
|
|
81
|
+
//
|
|
82
|
+
// target loses `[`, `]`, `|`; newlines and tabs FOLD to a space. Interior
|
|
83
|
+
// spaces stay — posts and notes resolve by exact title
|
|
84
|
+
// (`resolveRefToken` in convex/references.ts), so stripping them
|
|
85
|
+
// turns `[[The card]]` into a reference that can never match.
|
|
86
|
+
// label loses the same three characters plus newlines outright. It is
|
|
87
|
+
// display text, so a fold would invent whitespace the title
|
|
88
|
+
// never had.
|
|
89
|
+
//
|
|
90
|
+
// Two callers, one spelling: the walker's write path
|
|
91
|
+
// (`editorWalker/toMdast.ts`) and the live paste/typing path
|
|
92
|
+
// (`src/components/editor/extensions/work-link.ts`). They were separate copies
|
|
93
|
+
// until card #255 — the walker's had witnesses and the extension's did not,
|
|
94
|
+
// which is the shape where one copy drifts and nothing says so.
|
|
95
|
+
export function cleanWikiTarget(value) {
|
|
96
|
+
return value
|
|
97
|
+
.replace(/[[\]|]/g, "")
|
|
98
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
99
|
+
.trim();
|
|
100
|
+
}
|
|
101
|
+
export function cleanWikiLabel(value) {
|
|
102
|
+
return value.replace(/[[\]|\r\n]/g, "").trim();
|
|
103
|
+
}
|
|
104
|
+
// The anchor's cleaner, and it is a THIRD one rather than a reuse of either
|
|
105
|
+
// above, for a reason worth stating: the anchor is the only segment whose own
|
|
106
|
+
// separator can appear inside it. A `#` surviving into an anchor would give
|
|
107
|
+
// `[[a#b#c]]` two readings, and the split below takes the first `#` — so the
|
|
108
|
+
// write side removes the character the read side would trip over, exactly as
|
|
109
|
+
// `cleanWikiTarget` removes the `|` that would open a label.
|
|
110
|
+
//
|
|
111
|
+
// Otherwise it follows the TARGET, not the label: interior spaces stay, and
|
|
112
|
+
// newlines/tabs fold to a space. An anchor is matched against heading text
|
|
113
|
+
// (`toAnchorSlug` collapses the spaces at resolve time), so `#Open questions`
|
|
114
|
+
// must stay two words rather than becoming `Openquestions`.
|
|
115
|
+
export function cleanWikiAnchor(value) {
|
|
116
|
+
return value
|
|
117
|
+
.replace(/[[\]|#]/g, "")
|
|
118
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Split a target-with-anchor into its two halves, honouring the prefix table.
|
|
123
|
+
*
|
|
124
|
+
* The `#` is only a separator after at least one character of the BARE id, and
|
|
125
|
+
* that is not a nicety — `[[c:#42]]` is the existing "card by number" form
|
|
126
|
+
* (`parseWikiTarget` yields `{kind:"card", id:"#42"}`). Splitting on the first
|
|
127
|
+
* `#` anywhere would turn it into a link to the prefix `c:` with an anchor of
|
|
128
|
+
* `42`, silently unlinking every task referenced by number. So: find the
|
|
129
|
+
* prefix, then look for a `#` past the first character after it.
|
|
130
|
+
*
|
|
131
|
+
* An empty anchor (`[[n:abc#]]`) is NOT an anchor. The trailing `#` stays part
|
|
132
|
+
* of the target, which resolves to nothing and renders as the muted
|
|
133
|
+
* unknown-entity text the grammar already gives any unresolvable target —
|
|
134
|
+
* open-knowledge's tokenizer takes the same position from the other side, by
|
|
135
|
+
* refusing to close a wiki link on a zero-length anchor at all.
|
|
136
|
+
*/
|
|
137
|
+
export function splitWikiAnchor(target) {
|
|
138
|
+
const prefix = WIKI_LINK_PREFIXES.find(([p]) => target.startsWith(p))?.[0] ?? "";
|
|
139
|
+
const hash = target.indexOf("#", prefix.length + 1);
|
|
140
|
+
if (hash < 0)
|
|
141
|
+
return { target, anchor: null };
|
|
142
|
+
const anchor = target.slice(hash + 1).trim();
|
|
143
|
+
if (!anchor)
|
|
144
|
+
return { target, anchor: null };
|
|
145
|
+
return { target: target.slice(0, hash).trim(), anchor };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The BLOCK id an anchor addresses, or null when the anchor names a heading.
|
|
149
|
+
*
|
|
150
|
+
* One character decides it, and the split is done here rather than at each
|
|
151
|
+
* consumer so "does this token point at a block" has one answer. Three cases
|
|
152
|
+
* are deliberate:
|
|
153
|
+
*
|
|
154
|
+
* `^k7f3a2cx` → `"k7f3a2cx"`. The ordinal form of a duplicated block,
|
|
155
|
+
* `^k7f3a2cx.2`, comes back whole — the dot is part of the id
|
|
156
|
+
* (`fingerprintBlocks`), not a fourth separator.
|
|
157
|
+
* `Rollout` → null. A heading, and the reader slugs it as it always has.
|
|
158
|
+
* `^` → null. A marker with nothing after it addresses no block, the
|
|
159
|
+
* same position `splitWikiAnchor` takes on `[[n:abc#]]`: an
|
|
160
|
+
* empty half is not a half. It stays a (meaningless) heading
|
|
161
|
+
* anchor rather than becoming a block id of `""`, because an id
|
|
162
|
+
* nothing can resolve is worse than an anchor that resolves to
|
|
163
|
+
* no heading.
|
|
164
|
+
*
|
|
165
|
+
* The cost, stated: a heading whose text genuinely begins with `^` cannot be
|
|
166
|
+
* addressed as a heading. That is inherent in a one-character prefix and it is
|
|
167
|
+
* Obsidian's trade too; the alternative — a separate segment — would have every
|
|
168
|
+
* consumer of `anchor` learn a new shape to gain a case nobody writes.
|
|
169
|
+
*/
|
|
170
|
+
export function blockAnchorId(anchor) {
|
|
171
|
+
if (!anchor || !anchor.startsWith(BLOCK_ANCHOR_MARKER))
|
|
172
|
+
return null;
|
|
173
|
+
return anchor.slice(BLOCK_ANCHOR_MARKER.length).trim() || null;
|
|
174
|
+
}
|
|
175
|
+
/** The anchor bytes that address `blockId` — the inverse of `blockAnchorId`. */
|
|
176
|
+
export function blockAnchor(blockId) {
|
|
177
|
+
return `${BLOCK_ANCHOR_MARKER}${blockId}`;
|
|
178
|
+
}
|
|
179
|
+
/** Re-join a target and anchor into the bytes that go between the brackets. */
|
|
180
|
+
export function joinWikiAnchor(target, anchor) {
|
|
181
|
+
return anchor ? `${target}#${anchor}` : target;
|
|
182
|
+
}
|
|
183
|
+
// A fresh global matcher each call: a shared /g regex carries `lastIndex`
|
|
184
|
+
// between callers and would silently skip tokens.
|
|
185
|
+
//
|
|
186
|
+
// This one matches the BRACKETS ONLY, bang excluded, and that is load-bearing
|
|
187
|
+
// rather than an oversight. `canonicalizeReferences` (convex/references.ts)
|
|
188
|
+
// rewrites the span this pattern reports, so a span that had swallowed the bang
|
|
189
|
+
// would rewrite an embed into a link on the first canonicalisation pass. Reach
|
|
190
|
+
// for `wikiTokenPattern()` when the bang is part of what you are reading.
|
|
191
|
+
export function wikiLinkPattern() {
|
|
192
|
+
return /\[\[([^\]]+)\]\]/g;
|
|
193
|
+
}
|
|
194
|
+
// Link and embed together: the marker in group 1, the token body in group 2.
|
|
195
|
+
// The `!` is optional, so every `[[…]]` this finds `wikiLinkPattern()` finds
|
|
196
|
+
// too — the two never disagree about where a token is, only about whether the
|
|
197
|
+
// bang belongs to it.
|
|
198
|
+
export function wikiTokenPattern() {
|
|
199
|
+
return /(!?)\[\[([^\]]+)\]\]/g;
|
|
200
|
+
}
|
|
201
|
+
// Split a token body (the text between `[[` and `]]`) into target + anchor +
|
|
202
|
+
// label. Order matters: the pipe is found first, so a `#` in a label is just a
|
|
203
|
+
// character; then the anchor is split off what is left.
|
|
204
|
+
//
|
|
205
|
+
// `embed` is passed IN rather than sniffed: the body between the brackets is
|
|
206
|
+
// identical either way, and the caller is the only one holding the bang.
|
|
207
|
+
export function parseWikiToken(raw, embed = false) {
|
|
208
|
+
const pipe = raw.indexOf("|");
|
|
209
|
+
const written = (pipe >= 0 ? raw.slice(0, pipe) : raw).trim();
|
|
210
|
+
const explicit = pipe >= 0 ? raw.slice(pipe + 1).trim() : "";
|
|
211
|
+
const { target, anchor } = splitWikiAnchor(written);
|
|
212
|
+
const { kind, id } = parseWikiTarget(target);
|
|
213
|
+
return {
|
|
214
|
+
kind,
|
|
215
|
+
id,
|
|
216
|
+
target,
|
|
217
|
+
anchor,
|
|
218
|
+
blockId: blockAnchorId(anchor),
|
|
219
|
+
// Still the TARGET, not the target-plus-anchor. A label-less
|
|
220
|
+
// `[[n:abc#Rollout]]` displays the doc's resolved title (or its id), the
|
|
221
|
+
// same as `[[n:abc]]` does — the anchor is a destination, not a name.
|
|
222
|
+
label: explicit || target,
|
|
223
|
+
hasLabel: explicit.length > 0,
|
|
224
|
+
embed,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Classify a target (prefix included) into its kind + bare identifier.
|
|
228
|
+
export function parseWikiTarget(target) {
|
|
229
|
+
for (const [prefix, kind] of WIKI_LINK_PREFIXES) {
|
|
230
|
+
if (target.startsWith(prefix)) {
|
|
231
|
+
return { kind, id: target.slice(prefix.length) };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { kind: "post", id: target };
|
|
235
|
+
}
|
|
236
|
+
// Every `[[…]]` and `![[…]]` token in a body, in document order, not deduped.
|
|
237
|
+
//
|
|
238
|
+
// Embeds are INCLUDED, each flagged by its own `embed`. Callers that build the
|
|
239
|
+
// links graph want them: `![[c:42]]` references the card exactly as `[[c:42]]`
|
|
240
|
+
// does, and dropping embeds here would unlink every entity a document shows
|
|
241
|
+
// instead of naming.
|
|
242
|
+
export function parseWikiLinks(body) {
|
|
243
|
+
const out = [];
|
|
244
|
+
const re = wikiTokenPattern();
|
|
245
|
+
let m;
|
|
246
|
+
while ((m = re.exec(body)) !== null) {
|
|
247
|
+
out.push(parseWikiToken(m[2], m[1] === WIKI_EMBED_MARKER));
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
// Parse a string that must be EXACTLY one LINK and nothing else — the test
|
|
252
|
+
// behind "this whole line is a single link, so unfurl it as a card".
|
|
253
|
+
//
|
|
254
|
+
// Strict about the bang on purpose: an embed on its own line is a different
|
|
255
|
+
// rendering decision (it shows the entity because the author asked, not because
|
|
256
|
+
// the line happened to hold one link), and the two callers of this in the
|
|
257
|
+
// reader would otherwise stop being able to tell them apart.
|
|
258
|
+
export function matchWikiToken(text) {
|
|
259
|
+
const m = /^\[\[([^\]]+)\]\]$/.exec(text.trim());
|
|
260
|
+
return m ? parseWikiToken(m[1]) : null;
|
|
261
|
+
}
|
|
262
|
+
// The same test for an embed: exactly `![[…]]`, nothing else on the line.
|
|
263
|
+
export function matchWikiEmbed(text) {
|
|
264
|
+
const m = /^!\[\[([^\]]+)\]\]$/.exec(text.trim());
|
|
265
|
+
return m ? parseWikiToken(m[1], true) : null;
|
|
266
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* server so agents operate sfora natively.
|
|
9
9
|
*/
|
|
10
10
|
import { Bash, ReadWriteFs } from "just-bash";
|
|
11
|
+
import { SforaApiClient } from "./api-client.js";
|
|
11
12
|
import { SforaFs } from "./SforaFs.js";
|
|
13
|
+
import { type PresenceNotice } from "./block-commands.js";
|
|
12
14
|
export interface CreateSforaShellOptions {
|
|
13
15
|
/** Base URL of the sfora deployment, e.g. `http://localhost:2222`. */
|
|
14
16
|
baseUrl: string;
|
|
@@ -23,10 +25,27 @@ export interface CreateSforaShellOptions {
|
|
|
23
25
|
cwd?: string;
|
|
24
26
|
/** Act/post as an owned agent, using this key (sent as `X-Sfora-Act-As`). */
|
|
25
27
|
actAs?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The run's "you are visible" latch. Pass one when something OUTSIDE this
|
|
30
|
+
* shell can print the note too — the CLI does, after any line that wrote —
|
|
31
|
+
* so the two share a single "once". Omitted, the shell owns its own.
|
|
32
|
+
*/
|
|
33
|
+
presence?: PresenceNotice;
|
|
26
34
|
}
|
|
27
35
|
export interface SforaShell {
|
|
28
36
|
bash: Bash;
|
|
29
37
|
fs: SforaFs;
|
|
38
|
+
/**
|
|
39
|
+
* The transport the shell and fs share.
|
|
40
|
+
*
|
|
41
|
+
* Exposed so the CLI can read what the LAST request reported about itself —
|
|
42
|
+
* the page a response named, the effect report a write returned — without
|
|
43
|
+
* making a second one. `IFileSystem` has nowhere to put either, and a
|
|
44
|
+
* separately-constructed client would be watching a different conversation.
|
|
45
|
+
*/
|
|
46
|
+
client: SforaApiClient;
|
|
47
|
+
/** The latch the shell's commands announce presence through. */
|
|
48
|
+
presence: PresenceNotice;
|
|
30
49
|
}
|
|
31
50
|
export declare function createSforaShell(options: CreateSforaShellOptions): SforaShell;
|
|
32
51
|
export interface LocalShell {
|
|
@@ -43,4 +62,10 @@ export declare function createLocalShell(root: string, cwd?: string): LocalShell
|
|
|
43
62
|
export { SforaFs } from "./SforaFs.js";
|
|
44
63
|
export { LocalWorkspace, initWorkspace, findWorkspace, WORKSPACE_DIR, type TaskEntry, type TaskWriteResult, } from "./local/workspace.js";
|
|
45
64
|
export * from "./format/index.js";
|
|
46
|
-
export { SforaApiClient, SforaApiError, type SforaApiConfig, type Project, type Entry, type PostKind, type WriteResult, } from "./api-client.js";
|
|
65
|
+
export { SforaApiClient, SforaApiError, blockConflictFrom, writeEffectFrom, type SforaApiConfig, type Project, type Entry, type PostKind, type WriteResult, type WriteOptions, type WriteEffect, type RebindCounts, type ResponseInfo, type BlockView, type BlocksView, type BlockConflict, type BlockSummary, type AgentEvent, type AgentEventsPage, type DocPresence, type DocPresenceMember, } from "./api-client.js";
|
|
66
|
+
export { WEB_URL_HEADER, fsRequestPath, webUrlFromResponse, } from "./web-url.js";
|
|
67
|
+
export { openerCommand, type OpenerCommand } from "./opener.js";
|
|
68
|
+
export { blocksCommand, putCommand, urlCommand, resolveFsPath, presenceNotice, type CommandOutput, type PresenceNotice, } from "./block-commands.js";
|
|
69
|
+
export { sforaShellCommands, parseShellArgs } from "./shell-commands.js";
|
|
70
|
+
export { watchLoop, parseWatchTarget, MAX_BACKOFF_MS, type WatchDeps, type WatchOptions, type WatchTarget, } from "./watch.js";
|
|
71
|
+
export { renderPing, renderBlocks, renderBlockConflict, renderWriteEffect, ndjson, type DocPing, } from "./render.js";
|
package/dist/index.js
CHANGED
|
@@ -10,20 +10,31 @@
|
|
|
10
10
|
import { Bash, ReadWriteFs } from "just-bash";
|
|
11
11
|
import { SforaApiClient } from "./api-client.js";
|
|
12
12
|
import { SforaFs } from "./SforaFs.js";
|
|
13
|
+
import { sforaShellCommands } from "./shell-commands.js";
|
|
14
|
+
import { presenceNotice } from "./block-commands.js";
|
|
13
15
|
export function createSforaShell(options) {
|
|
14
16
|
const client = new SforaApiClient({
|
|
15
17
|
baseUrl: options.baseUrl,
|
|
16
18
|
apiKey: options.apiKey,
|
|
17
19
|
actAs: options.actAs,
|
|
18
20
|
});
|
|
21
|
+
const presence = options.presence ?? presenceNotice();
|
|
19
22
|
const fs = new SforaFs(client);
|
|
20
23
|
// Disable just-bash's in-process defense-in-depth sandbox. It defaults on and
|
|
21
24
|
// hardens against *untrusted* scripts by blocking globals like `WeakRef` —
|
|
22
25
|
// which undici's `fetch` uses internally, so it would break every network-backed
|
|
23
26
|
// fs call. sfora runs the user's / agent's own commands against their own
|
|
24
27
|
// workspace over HTTPS, so this hardening is unnecessary here.
|
|
25
|
-
const bash = new Bash({
|
|
26
|
-
|
|
28
|
+
const bash = new Bash({
|
|
29
|
+
fs,
|
|
30
|
+
cwd: options.cwd ?? "/",
|
|
31
|
+
defenseInDepth: false,
|
|
32
|
+
// sfora's own verbs, inside the shell (card #333). They are registered
|
|
33
|
+
// rather than special-cased in the REPL so they compose like every other
|
|
34
|
+
// command: `blocks x.md | grep heading`, `sed … | put x.md --block <id>`.
|
|
35
|
+
customCommands: sforaShellCommands(client, presence),
|
|
36
|
+
});
|
|
37
|
+
return { bash, fs, client, presence };
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
40
|
* Shell over a local `.sfora/` workspace — the OSS serverless mode. The same
|
|
@@ -39,4 +50,10 @@ export function createLocalShell(root, cwd = "/") {
|
|
|
39
50
|
export { SforaFs } from "./SforaFs.js";
|
|
40
51
|
export { LocalWorkspace, initWorkspace, findWorkspace, WORKSPACE_DIR, } from "./local/workspace.js";
|
|
41
52
|
export * from "./format/index.js";
|
|
42
|
-
export { SforaApiClient, SforaApiError, } from "./api-client.js";
|
|
53
|
+
export { SforaApiClient, SforaApiError, blockConflictFrom, writeEffectFrom, } from "./api-client.js";
|
|
54
|
+
export { WEB_URL_HEADER, fsRequestPath, webUrlFromResponse, } from "./web-url.js";
|
|
55
|
+
export { openerCommand } from "./opener.js";
|
|
56
|
+
export { blocksCommand, putCommand, urlCommand, resolveFsPath, presenceNotice, } from "./block-commands.js";
|
|
57
|
+
export { sforaShellCommands, parseShellArgs } from "./shell-commands.js";
|
|
58
|
+
export { watchLoop, parseWatchTarget, MAX_BACKOFF_MS, } from "./watch.js";
|
|
59
|
+
export { renderPing, renderBlocks, renderBlockConflict, renderWriteEffect, ndjson, } from "./render.js";
|
package/dist/mcp-server.js
CHANGED
|
@@ -14,9 +14,12 @@ const TOOL_DESCRIPTION = `Run a bash command against the sfora workspace — a U
|
|
|
14
14
|
- /projects/<slug>/posts/<file>.md published posts
|
|
15
15
|
- /projects/<slug>/drafts/<file>.md your drafts
|
|
16
16
|
- /projects/<slug>/board/<NN-stage>/<NNNN>.md tasks (kanban cards), by stage
|
|
17
|
-
- /projects/<slug>/
|
|
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
|
|
19
|
-
- /projects/<slug>/plan.md the goal + open questions
|
|
21
|
+
- /projects/<slug>/plan.md the goal + open questions (write to set the goal)
|
|
22
|
+
- /projects/<slug>/asks.md coordination asks, read-only
|
|
20
23
|
- /inbox/mentions.md unread mentions
|
|
21
24
|
- /me/api-key your identity
|
|
22
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.
|
package/dist/opener.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handing a URL to the desktop — the one command per platform, as data.
|
|
3
|
+
*
|
|
4
|
+
* Split out from the spawn so it can be tested: what a test can meaningfully
|
|
5
|
+
* assert about `sfora open` is that it builds the RIGHT command for the
|
|
6
|
+
* platform, not that a browser appeared. `openerCommand` is pure, so the three
|
|
7
|
+
* platforms are three assertions; `openUrl` is the four-line side effect that
|
|
8
|
+
* cannot be, and holds nothing worth testing.
|
|
9
|
+
*/
|
|
10
|
+
export interface OpenerCommand {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The opener for a platform, as `NodeJS.Process["platform"]` spells it.
|
|
16
|
+
*
|
|
17
|
+
* Windows is the case worth stating: `start` is not an executable, it is a
|
|
18
|
+
* `cmd.exe` builtin, so spawning it directly fails with ENOENT. It has to be
|
|
19
|
+
* run THROUGH cmd — and the empty string after `start` is not padding: `start`
|
|
20
|
+
* reads its first quoted argument as the new window's TITLE, so a URL in quotes
|
|
21
|
+
* would open a console window titled with the link instead of the link.
|
|
22
|
+
*/
|
|
23
|
+
export declare function openerCommand(platform: NodeJS.Platform | string, url: string): OpenerCommand;
|
package/dist/opener.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handing a URL to the desktop — the one command per platform, as data.
|
|
3
|
+
*
|
|
4
|
+
* Split out from the spawn so it can be tested: what a test can meaningfully
|
|
5
|
+
* assert about `sfora open` is that it builds the RIGHT command for the
|
|
6
|
+
* platform, not that a browser appeared. `openerCommand` is pure, so the three
|
|
7
|
+
* platforms are three assertions; `openUrl` is the four-line side effect that
|
|
8
|
+
* cannot be, and holds nothing worth testing.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The opener for a platform, as `NodeJS.Process["platform"]` spells it.
|
|
12
|
+
*
|
|
13
|
+
* Windows is the case worth stating: `start` is not an executable, it is a
|
|
14
|
+
* `cmd.exe` builtin, so spawning it directly fails with ENOENT. It has to be
|
|
15
|
+
* run THROUGH cmd — and the empty string after `start` is not padding: `start`
|
|
16
|
+
* reads its first quoted argument as the new window's TITLE, so a URL in quotes
|
|
17
|
+
* would open a console window titled with the link instead of the link.
|
|
18
|
+
*/
|
|
19
|
+
export function openerCommand(platform, url) {
|
|
20
|
+
if (platform === "darwin")
|
|
21
|
+
return { command: "open", args: [url] };
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
return { command: "cmd", args: ["/c", "start", "", url] };
|
|
24
|
+
}
|
|
25
|
+
return { command: "xdg-open", args: [url] };
|
|
26
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the CLI prints — as pure functions, so it can be tested.
|
|
3
|
+
*
|
|
4
|
+
* Everything here takes data and returns a string. No I/O, no process, no
|
|
5
|
+
* colours decided by a TTY check: the caller writes the result. That is what
|
|
6
|
+
* lets `render.test.ts` assert on a 409 re-aim table or a blocks listing
|
|
7
|
+
* without a server, a terminal, or a snapshot of the whole command.
|
|
8
|
+
*
|
|
9
|
+
* The rule these follow, from the mission's binding: THE CLI PRINTS WHAT THE
|
|
10
|
+
* SERVER SENDS. Nothing here computes a URL, a route, or a block id — each
|
|
11
|
+
* comes off a response and is formatted.
|
|
12
|
+
*/
|
|
13
|
+
import type { BlockConflict, BlockView, BlocksView, WriteEffect } from "./api-client.js";
|
|
14
|
+
export declare const colors: {
|
|
15
|
+
reset: string;
|
|
16
|
+
bold: string;
|
|
17
|
+
dim: string;
|
|
18
|
+
cyan: string;
|
|
19
|
+
green: string;
|
|
20
|
+
yellow: string;
|
|
21
|
+
blue: string;
|
|
22
|
+
red: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A link, as one quiet line.
|
|
26
|
+
*
|
|
27
|
+
* Dim and unadorned because it is an offer, not an instruction: the line a
|
|
28
|
+
* reader's eye skips until the moment they want it, and a terminal that
|
|
29
|
+
* hyperlinks URLs makes it clickable without any markup from us.
|
|
30
|
+
*/
|
|
31
|
+
export declare function urlLine(url: string): string;
|
|
32
|
+
/** The first line of a block's text, collapsed and clipped for one row. */
|
|
33
|
+
export declare function blockSummary(block: BlockView, width?: number): string;
|
|
34
|
+
/**
|
|
35
|
+
* `sfora blocks <path>` — the addressable view, one row per block.
|
|
36
|
+
*
|
|
37
|
+
* The id comes FIRST because it is the only column anyone retypes: the next
|
|
38
|
+
* command is `sfora put <path> --block <id>`, and a listing that buries the id
|
|
39
|
+
* makes the reader hunt for it.
|
|
40
|
+
*
|
|
41
|
+
* `writable: false` is stated, not omitted, and it is stated as "read-only"
|
|
42
|
+
* rather than by leaving the row out. Those blocks are real — the frontmatter
|
|
43
|
+
* fence, the title heading — they occupy lines the reader can see in the file,
|
|
44
|
+
* and hiding them would make the listing disagree with `cat`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function renderBlocks(view: BlocksView): string;
|
|
47
|
+
/**
|
|
48
|
+
* The 409 a `?block=` write gets: the id named nothing, here is what the
|
|
49
|
+
* document has now.
|
|
50
|
+
*
|
|
51
|
+
* A table and not a message, because the recovery IS the payload: the server
|
|
52
|
+
* already sent the current blocks, so the fix is one glance and one retry
|
|
53
|
+
* rather than a second `sfora blocks` round trip. The retry line is spelled
|
|
54
|
+
* out with the real path for the same reason.
|
|
55
|
+
*
|
|
56
|
+
* THE COLUMNS ARE THE PAYLOAD'S — id, line, preview — and not `sfora blocks`'s.
|
|
57
|
+
* The two lists describe different bytes (see {@link BlockSummary}): every id
|
|
58
|
+
* here already resolves at the write door, so there is no writable/read-only
|
|
59
|
+
* distinction to draw and no node type in the payload to draw one with. An
|
|
60
|
+
* earlier version of this function reached for both anyway and printed
|
|
61
|
+
* `undefined read-only` on every row, which inverted the one fact the table
|
|
62
|
+
* exists to convey. `line` earns the column it took: it is where in the file
|
|
63
|
+
* the caller is holding to look, and it is the field that tells two blocks with
|
|
64
|
+
* the same opening words apart.
|
|
65
|
+
*/
|
|
66
|
+
export declare function renderBlockConflict(conflict: BlockConflict, fsPath: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* The effect report, as one line — printed after EVERY write.
|
|
69
|
+
*
|
|
70
|
+
* A bare "saved" is a lie the CLI used to tell: sfora's write door splices, so
|
|
71
|
+
* a PUT of bytes that parse the same as the stored ones stores nothing and
|
|
72
|
+
* `changed: false` is a normal answer. Saying so is the difference between an
|
|
73
|
+
* agent that knows its edit landed and one that assumes it did.
|
|
74
|
+
*
|
|
75
|
+
* The rebind counts describe the blocks the document had BEFORE this write:
|
|
76
|
+
* `total` of them, of which `rebound` are the same block under a new id and
|
|
77
|
+
* `orphaned` are blocks nothing in the new document can be shown to be. The
|
|
78
|
+
* rest kept the id they had, which is the number worth leading with — it is
|
|
79
|
+
* what tells a reader whether their block addresses still work.
|
|
80
|
+
*/
|
|
81
|
+
export declare function renderWriteEffect(effect: WriteEffect | null): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* The one-time "you are visible" note.
|
|
84
|
+
*
|
|
85
|
+
* Writing DECLARES PRESENCE server-side — the document's roster shows the
|
|
86
|
+
* writer, and a human with it open sees them. That is a fact about the user's
|
|
87
|
+
* visibility to other people, so it is said out loud rather than left to be
|
|
88
|
+
* discovered in the UI; once per run, because said on every write it would be
|
|
89
|
+
* noise and the second one teaches nothing.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PRESENCE_NOTE = "you are visible as editing this document";
|
|
92
|
+
/** One `doc.write` / `doc.delete` ping from `/v1/events`, as one line. */
|
|
93
|
+
export interface DocPing {
|
|
94
|
+
type: "doc.write" | "doc.delete";
|
|
95
|
+
ts: number;
|
|
96
|
+
docType?: string;
|
|
97
|
+
docId?: string;
|
|
98
|
+
title?: string;
|
|
99
|
+
path?: string;
|
|
100
|
+
url?: string;
|
|
101
|
+
author?: string;
|
|
102
|
+
authorType?: "human" | "agent";
|
|
103
|
+
changed?: boolean;
|
|
104
|
+
deleted?: boolean;
|
|
105
|
+
restricted?: boolean;
|
|
106
|
+
blockIds?: {
|
|
107
|
+
rebound: number;
|
|
108
|
+
orphaned: number;
|
|
109
|
+
total: number;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** `HH:MM:SS` in the reader's own timezone — a ping is read as it lands. */
|
|
113
|
+
export declare function pingTime(ts: number): string;
|
|
114
|
+
/**
|
|
115
|
+
* `time · author · doc · what changed · url` — one ping, one line.
|
|
116
|
+
*
|
|
117
|
+
* A RESTRICTED ping keeps its shape and loses its pointer: the server
|
|
118
|
+
* delivered the fact (a document you are watching moved, by whom) and withheld
|
|
119
|
+
* the title and the link because they belong to somebody's draft. Printing
|
|
120
|
+
* "(restricted)" rather than dropping the line is the honest half — a watch
|
|
121
|
+
* that went silent would look broken.
|
|
122
|
+
*/
|
|
123
|
+
export declare function renderPing(ping: DocPing): string;
|
|
124
|
+
/**
|
|
125
|
+
* The NDJSON line for one ping — the machine half of `sfora watch --json`.
|
|
126
|
+
*
|
|
127
|
+
* The SERVER'S event object, verbatim, plus nothing. A CLI that reshaped it
|
|
128
|
+
* would become a second schema to keep in step with `/v1/events`, and the one
|
|
129
|
+
* consumer that matters here (an agent piping this into a program) is better
|
|
130
|
+
* served by the wire shape it can also get from the HTTP door directly.
|
|
131
|
+
*/
|
|
132
|
+
export declare function ndjson(event: unknown): string;
|