wiki-viewer 1.4.3 → 1.5.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +34 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +8 -7
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js +2 -2
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js +3 -3
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
- package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0y9k6h~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0znncy9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_12-ni1n._.js +1 -1
- package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 14luv~jq7e6-n.js} +12 -12
- package/.next/standalone/README.md +79 -16
- package/.next/standalone/agents/bootstrap-prompt.md +12 -1
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
- package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
- package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
- package/.next/standalone/pnpm-lock.yaml +9 -9
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
- package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
- package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
- package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
- package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
- package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
- package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
- package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +49 -0
- package/.next/standalone/src/components/editor/editor.tsx +38 -0
- package/.next/standalone/src/lib/proof/audit.ts +79 -0
- package/.next/standalone/src/lib/proof/auth.ts +2 -2
- package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
- package/.next/standalone/src/lib/proof/lease.ts +104 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
- package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
- package/.next/standalone/src/lib/proof/registry.ts +1 -1
- package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
- package/.next/standalone/src/lib/proof/types.ts +13 -1
- package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
- package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
- package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
- package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/README.md +79 -16
- package/package.json +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_ssgManifest.js +0 -0
|
@@ -68,8 +68,36 @@ export function AIPanel({ currentPath }: { currentPath?: string | null }) {
|
|
|
68
68
|
() => bootstrapPrompt.replace(/\$WIKI_URL/g, origin),
|
|
69
69
|
[bootstrapPrompt, origin]
|
|
70
70
|
);
|
|
71
|
+
const getMcpRegister = useCallback(
|
|
72
|
+
() =>
|
|
73
|
+
`npx wiki-viewer-mcp register --url ${origin} --id ai:myagent --name "My Agent"`,
|
|
74
|
+
[origin]
|
|
75
|
+
);
|
|
76
|
+
const getMcpJson = useCallback(
|
|
77
|
+
() =>
|
|
78
|
+
JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
servers: {
|
|
81
|
+
"wiki-viewer": {
|
|
82
|
+
command: "npx",
|
|
83
|
+
args: ["wiki-viewer-mcp"],
|
|
84
|
+
env: {
|
|
85
|
+
WIKI_VIEWER_URL: origin,
|
|
86
|
+
WIKI_VIEWER_TOKEN: "<token from register>",
|
|
87
|
+
WIKI_VIEWER_AGENT_ID: "ai:myagent",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
),
|
|
95
|
+
[origin]
|
|
96
|
+
);
|
|
71
97
|
const skillCli = useCopyButton(getSkillCli);
|
|
72
98
|
const bootstrapCopy = useCopyButton(getBootstrapPrompt);
|
|
99
|
+
const mcpRegister = useCopyButton(getMcpRegister);
|
|
100
|
+
const mcpJson = useCopyButton(getMcpJson);
|
|
73
101
|
|
|
74
102
|
// Keyboard: Esc closes
|
|
75
103
|
useEffect(() => {
|
|
@@ -182,6 +210,27 @@ export function AIPanel({ currentPath }: { currentPath?: string | null }) {
|
|
|
182
210
|
Install for AI Agents
|
|
183
211
|
</h3>
|
|
184
212
|
<div className="rounded-md border border-border bg-muted/40 p-3 space-y-3">
|
|
213
|
+
<div className="space-y-1">
|
|
214
|
+
<p className="text-[10px] text-muted-foreground/70">
|
|
215
|
+
MCP server <span className="text-muted-foreground/50">(Claude Code, Cursor, Codex):</span>
|
|
216
|
+
</p>
|
|
217
|
+
<p className="text-[10px] text-muted-foreground/50">1. Register & get a token:</p>
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<code className="flex-1 text-[10px] font-mono text-foreground/80 truncate bg-muted rounded px-1 py-0.5">
|
|
220
|
+
{getMcpRegister()}
|
|
221
|
+
</code>
|
|
222
|
+
<Button size="sm" variant="ghost" className="h-6 w-6 p-0 shrink-0" title="Copy register command" onClick={() => void mcpRegister.copy()}>
|
|
223
|
+
{mcpRegister.copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
<p className="text-[10px] text-muted-foreground/50 pt-1">2. Approve below, then add to your <code className="bg-muted px-0.5 rounded">mcp.json</code>:</p>
|
|
227
|
+
<Button size="sm" variant="outline" className="h-7 w-full text-xs gap-1.5" onClick={() => void mcpJson.copy()}>
|
|
228
|
+
{mcpJson.copied
|
|
229
|
+
? <><Check className="h-3.5 w-3.5 text-green-500" /> Copied!</>
|
|
230
|
+
: <><Copy className="h-3.5 w-3.5" /> Copy mcp.json</>}
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="h-px bg-border/60" />
|
|
185
234
|
<div className="space-y-1">
|
|
186
235
|
<p className="text-[10px] text-muted-foreground/70">Agent Skills standard:</p>
|
|
187
236
|
<div className="flex items-center gap-2">
|
|
@@ -184,6 +184,44 @@ export function KBEditor() {
|
|
|
184
184
|
void useProofStore.getState().loadSidecar(currentPath);
|
|
185
185
|
}, [currentPath]);
|
|
186
186
|
|
|
187
|
+
// Human edit-lease heartbeat: tell the server a human has this markdown doc
|
|
188
|
+
// open so computeCollabState() reports "active" even before the first
|
|
189
|
+
// suggestion/comment. This is what makes raw-fs writes from agents defer to
|
|
190
|
+
// the collaborative (Tier-2) path while a human is editing. Only markdown
|
|
191
|
+
// files participate in the collab-state machine.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (typeof window === "undefined") return;
|
|
194
|
+
if (!currentPath) return;
|
|
195
|
+
if (!/\.(md|markdown)$/i.test(currentPath)) return;
|
|
196
|
+
|
|
197
|
+
const path = currentPath;
|
|
198
|
+
const ping = (action: "open" | "heartbeat" | "close") => {
|
|
199
|
+
// keepalive lets the "close" beacon survive page unload.
|
|
200
|
+
void fetch("/api/wiki/presence", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ path, action }),
|
|
204
|
+
keepalive: action === "close",
|
|
205
|
+
}).catch(() => {
|
|
206
|
+
/* presence is best-effort; ignore failures */
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
ping("open");
|
|
211
|
+
// Refresh well within the 90s server lease TTL.
|
|
212
|
+
const id = setInterval(() => ping("heartbeat"), 30_000);
|
|
213
|
+
const onHidden = () => {
|
|
214
|
+
if (document.visibilityState === "hidden") ping("heartbeat");
|
|
215
|
+
};
|
|
216
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
217
|
+
|
|
218
|
+
return () => {
|
|
219
|
+
clearInterval(id);
|
|
220
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
221
|
+
ping("close");
|
|
222
|
+
};
|
|
223
|
+
}, [currentPath]);
|
|
224
|
+
|
|
187
225
|
// Subscribe to chokidar SSE: when current file changes on disk, reload sidecar.
|
|
188
226
|
useEffect(() => {
|
|
189
227
|
if (typeof window === "undefined") return;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal durable audit log for raw-fs mutations (Tier 1).
|
|
3
|
+
*
|
|
4
|
+
* Uses the same ~/.wiki-viewer/auth.db already opened by better-auth.
|
|
5
|
+
* Table: agent_fs_audit
|
|
6
|
+
*
|
|
7
|
+
* For .md files the sidecar event covers provenance; this covers ALL types.
|
|
8
|
+
*/
|
|
9
|
+
import Database from "better-sqlite3";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { mkdirSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
function dataDir(): string {
|
|
15
|
+
return path.join(process.env.HOME ?? os.homedir(), ".wiki-viewer");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Lazily initialised so tests can set HOME before first import.
|
|
19
|
+
let _db: InstanceType<typeof Database> | null = null;
|
|
20
|
+
|
|
21
|
+
function getDb(): InstanceType<typeof Database> {
|
|
22
|
+
if (_db) return _db;
|
|
23
|
+
const dir = dataDir();
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
const dbPath = path.join(dir, "auth.db");
|
|
26
|
+
_db = new Database(dbPath);
|
|
27
|
+
_db.pragma("journal_mode = WAL");
|
|
28
|
+
_db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS agent_fs_audit (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
agent_id TEXT NOT NULL,
|
|
32
|
+
op TEXT NOT NULL,
|
|
33
|
+
path TEXT NOT NULL,
|
|
34
|
+
old_sha TEXT,
|
|
35
|
+
new_sha TEXT,
|
|
36
|
+
forced INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
at TEXT NOT NULL
|
|
38
|
+
)
|
|
39
|
+
`);
|
|
40
|
+
return _db;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AuditRow {
|
|
44
|
+
agentId: string;
|
|
45
|
+
op: string;
|
|
46
|
+
path: string;
|
|
47
|
+
oldSha?: string;
|
|
48
|
+
newSha?: string;
|
|
49
|
+
forced?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write one audit row synchronously (SQLite sync is fine here — it's a WAL journal append).
|
|
54
|
+
* Fire-and-forget: never throws; logs to stderr on error.
|
|
55
|
+
*/
|
|
56
|
+
export function writeAuditRow(row: AuditRow): void {
|
|
57
|
+
try {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
db.prepare(
|
|
60
|
+
`INSERT INTO agent_fs_audit (agent_id, op, path, old_sha, new_sha, forced, at)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
62
|
+
).run(
|
|
63
|
+
row.agentId,
|
|
64
|
+
row.op,
|
|
65
|
+
row.path,
|
|
66
|
+
row.oldSha ?? null,
|
|
67
|
+
row.newSha ?? null,
|
|
68
|
+
row.forced ? 1 : 0,
|
|
69
|
+
new Date().toISOString(),
|
|
70
|
+
);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error("[agent-fs audit] write failed:", e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Reset cached DB handle (used by tests to pick up new HOME). */
|
|
77
|
+
export function _resetAuditDb(): void {
|
|
78
|
+
_db = null;
|
|
79
|
+
}
|
|
@@ -34,7 +34,7 @@ export async function checkAuth(req: Request): Promise<CheckAuthResult> {
|
|
|
34
34
|
id: `user:${u.id}`,
|
|
35
35
|
displayName: u.name,
|
|
36
36
|
tokenHash: "",
|
|
37
|
-
scope: { paths: ["**/*"], ops: ["read", "mutate"] },
|
|
37
|
+
scope: { paths: ["**/*"], ops: ["read", "mutate", "delete"] },
|
|
38
38
|
createdAt: "",
|
|
39
39
|
lastSeen: "",
|
|
40
40
|
},
|
|
@@ -71,7 +71,7 @@ export async function checkAuth(req: Request): Promise<CheckAuthResult> {
|
|
|
71
71
|
|
|
72
72
|
export interface ScopeParams {
|
|
73
73
|
filePath?: string;
|
|
74
|
-
op: "read" | "mutate";
|
|
74
|
+
op: "read" | "mutate" | "delete";
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function enforceScope(
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collab-state computation for §3.5 Working mode vs Collaborating mode.
|
|
3
|
+
*
|
|
4
|
+
* Returns:
|
|
5
|
+
* state — "active" | "tracked" | "untracked" | "not-markdown"
|
|
6
|
+
* revision — X-Collab-Revision (sidecar.revision + leaseGeneration(path))
|
|
7
|
+
* snapshotUrl — Tier-2 snapshot URL (null for non-markdown)
|
|
8
|
+
*
|
|
9
|
+
* Revision formula:
|
|
10
|
+
* revision = (sidecar?.revision ?? 0) + leaseGeneration(relPath)
|
|
11
|
+
*
|
|
12
|
+
* sidecar.revision bumps on every collab write (block-ops, comment, suggestion).
|
|
13
|
+
* leaseGeneration bumps on every lease open/close.
|
|
14
|
+
* Together they form a monotonically-non-decreasing integer that changes
|
|
15
|
+
* whenever collab state could change. A raw PUT that supplies
|
|
16
|
+
* If-Collab-Match: <n> is only safe when n equals the revision computed
|
|
17
|
+
* atomically inside the write mutex — otherwise 409 COLLAB_ACTIVE.
|
|
18
|
+
*
|
|
19
|
+
* active = has artifacts (pendingSuggestions > 0 OR unresolvedComments > 0
|
|
20
|
+
* OR blockProvenance entries > 0) OR has a current human edit lease.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readSidecar } from "./sidecar";
|
|
24
|
+
import { hasActiveLease, leaseGeneration } from "./lease";
|
|
25
|
+
|
|
26
|
+
function isMarkdownPath(p: string): boolean {
|
|
27
|
+
return p.endsWith(".md") || p.endsWith(".markdown");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CollabState = "active" | "tracked" | "untracked" | "not-markdown";
|
|
31
|
+
|
|
32
|
+
export interface CollabStateResult {
|
|
33
|
+
state: CollabState;
|
|
34
|
+
/** sidecar.revision + leaseGeneration(relPath) */
|
|
35
|
+
revision: number;
|
|
36
|
+
/** /api/agent/files/<relPath> — null for not-markdown */
|
|
37
|
+
snapshotUrl: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function computeCollabState(
|
|
41
|
+
rootDir: string,
|
|
42
|
+
relPath: string,
|
|
43
|
+
): Promise<CollabStateResult> {
|
|
44
|
+
if (!isMarkdownPath(relPath)) {
|
|
45
|
+
return { state: "not-markdown", revision: 0, snapshotUrl: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const snapshotUrl = `/api/agent/files/${relPath}`;
|
|
49
|
+
|
|
50
|
+
// Compute revision components
|
|
51
|
+
const sidecar = await readSidecar(rootDir, relPath);
|
|
52
|
+
const gen = leaseGeneration(relPath);
|
|
53
|
+
const sidecarRevision = sidecar?.revision ?? 0;
|
|
54
|
+
const revision = sidecarRevision + gen;
|
|
55
|
+
|
|
56
|
+
// Lease check — do after reading sidecar to keep gen stable within this call
|
|
57
|
+
const leaseActive = hasActiveLease(relPath);
|
|
58
|
+
|
|
59
|
+
if (!sidecar && !leaseActive) {
|
|
60
|
+
return { state: "untracked", revision, snapshotUrl };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (leaseActive) {
|
|
64
|
+
return { state: "active", revision, snapshotUrl };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Has sidecar, no lease — inspect artifacts
|
|
68
|
+
const pendingSuggestions = sidecar!.suggestions.filter(
|
|
69
|
+
(s) => s.status === "pending" && !s.stale,
|
|
70
|
+
).length;
|
|
71
|
+
const unresolvedComments = sidecar!.comments.filter((c) => !c.resolved).length;
|
|
72
|
+
const proofSpanCount = Object.keys(sidecar!.blockProvenance ?? {}).length;
|
|
73
|
+
|
|
74
|
+
if (pendingSuggestions > 0 || unresolvedComments > 0 || proofSpanCount > 0) {
|
|
75
|
+
return { state: "active", revision, snapshotUrl };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { state: "tracked", revision, snapshotUrl };
|
|
79
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human edit-lease store (in-memory, globalThis for hot-reload survival).
|
|
3
|
+
*
|
|
4
|
+
* A lease is set when a human opens a .md doc in the editor and refreshed
|
|
5
|
+
* via heartbeat while the doc stays open. Its presence makes collab-state
|
|
6
|
+
* "active" even before the human has typed a single suggestion (closes the
|
|
7
|
+
* false-negative GPT-5.5 caught in the design review).
|
|
8
|
+
*
|
|
9
|
+
* TTL: LEASE_TTL_MS (default 90 s). Editor should heartbeat every ~30 s.
|
|
10
|
+
*
|
|
11
|
+
* Collab-generation counter:
|
|
12
|
+
* - Starts at 0 per path.
|
|
13
|
+
* - Increments on EVERY 0→1 transition (first setLease, or setLease after
|
|
14
|
+
* all prior leases expired) AND on every 1→0 transition (clearLease or
|
|
15
|
+
* expiry-on-read).
|
|
16
|
+
* - X-Collab-Revision = sidecar.revision + leaseGeneration(path).
|
|
17
|
+
* Any lease or sidecar state change therefore bumps the combined revision.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const LEASE_TTL_MS = 90_000;
|
|
21
|
+
|
|
22
|
+
interface LeaseEntry {
|
|
23
|
+
userId: string;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface LeaseStore {
|
|
28
|
+
leases: Map<string, LeaseEntry>;
|
|
29
|
+
generations: Map<string, number>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const g = globalThis as typeof globalThis & { __wikiLeaseStore?: LeaseStore };
|
|
33
|
+
if (!g.__wikiLeaseStore) {
|
|
34
|
+
g.__wikiLeaseStore = { leases: new Map(), generations: new Map() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function store(): LeaseStore {
|
|
38
|
+
return g.__wikiLeaseStore!;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True if relPath has a non-expired lease RIGHT NOW (lazy-sweeps on check). */
|
|
42
|
+
function isLeaseActiveInternal(relPath: string): boolean {
|
|
43
|
+
const s = store();
|
|
44
|
+
const entry = s.leases.get(relPath);
|
|
45
|
+
if (!entry) return false;
|
|
46
|
+
if (Date.now() >= entry.expiresAt) {
|
|
47
|
+
s.leases.delete(relPath);
|
|
48
|
+
// 1→0 transition: bump generation
|
|
49
|
+
s.generations.set(relPath, (s.generations.get(relPath) ?? 0) + 1);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set (or refresh) a lease for relPath by userId.
|
|
57
|
+
* Bumps the generation counter on a 0→1 transition.
|
|
58
|
+
*/
|
|
59
|
+
export function setLease(relPath: string, userId: string, ttlMs = LEASE_TTL_MS): void {
|
|
60
|
+
const s = store();
|
|
61
|
+
const wasActive = isLeaseActiveInternal(relPath);
|
|
62
|
+
s.leases.set(relPath, { userId, expiresAt: Date.now() + ttlMs });
|
|
63
|
+
if (!wasActive) {
|
|
64
|
+
// 0→1 transition: bump generation
|
|
65
|
+
s.generations.set(relPath, (s.generations.get(relPath) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True if relPath has a current non-expired lease. */
|
|
70
|
+
export function hasActiveLease(relPath: string): boolean {
|
|
71
|
+
return isLeaseActiveInternal(relPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear the lease for relPath (on doc close).
|
|
76
|
+
* Only clears if the existing lease belongs to userId.
|
|
77
|
+
* Bumps generation on removal.
|
|
78
|
+
*/
|
|
79
|
+
export function clearLease(relPath: string, userId: string): void {
|
|
80
|
+
const s = store();
|
|
81
|
+
const entry = s.leases.get(relPath);
|
|
82
|
+
if (!entry) return;
|
|
83
|
+
// Still clear expired leases owned by the same user
|
|
84
|
+
if (entry.userId === userId) {
|
|
85
|
+
s.leases.delete(relPath);
|
|
86
|
+
// 1→0 transition: bump generation
|
|
87
|
+
s.generations.set(relPath, (s.generations.get(relPath) ?? 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the monotonic-ish collab generation for relPath.
|
|
93
|
+
* Increments on every 0→1 and 1→0 lease transition.
|
|
94
|
+
*/
|
|
95
|
+
export function leaseGeneration(relPath: string): number {
|
|
96
|
+
return store().generations.get(relPath) ?? 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Reset store — for tests only. */
|
|
100
|
+
export function _resetLeaseStore(): void {
|
|
101
|
+
const s = store();
|
|
102
|
+
s.leases.clear();
|
|
103
|
+
s.generations.clear();
|
|
104
|
+
}
|
|
@@ -103,6 +103,20 @@ function opMarkdownToBlocks(
|
|
|
103
103
|
return { nodes, refs };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function markOrphanedRefsStale(sidecar: Sidecar, newRefMap: Record<string, unknown>): void {
|
|
107
|
+
const validRefs = new Set(Object.keys(newRefMap));
|
|
108
|
+
for (const s of sidecar.suggestions) {
|
|
109
|
+
if (s.status === "pending" && !validRefs.has(s.ref)) {
|
|
110
|
+
s.stale = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const c of sidecar.comments) {
|
|
114
|
+
if (!c.resolved && !validRefs.has(c.ref)) {
|
|
115
|
+
c.stale = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
function buildSnapshot(
|
|
107
121
|
mdPath: string,
|
|
108
122
|
blocks: Block[],
|
|
@@ -121,6 +135,49 @@ function buildSnapshot(
|
|
|
121
135
|
};
|
|
122
136
|
}
|
|
123
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Reconcile a sidecar after a file was modified outside of block-ops.
|
|
140
|
+
* Rebuilds refMap, bumps revision, marks orphaned anchors stale, emits event, writes sidecar.
|
|
141
|
+
*
|
|
142
|
+
* Callers MUST hold `withFileMutex(mdPath, ...)` before calling.
|
|
143
|
+
*
|
|
144
|
+
* Used by:
|
|
145
|
+
* - readSnapshot: eventType="file.externallyEdited", by="system"
|
|
146
|
+
* - raw-fs PUT (Phase 2): eventType="file.rawWritten", by="ai:<id>"
|
|
147
|
+
*/
|
|
148
|
+
export async function reconcileSidecar(args: {
|
|
149
|
+
rootDir: string;
|
|
150
|
+
mdPath: string;
|
|
151
|
+
content: string;
|
|
152
|
+
sidecar: Sidecar; // mutated in place
|
|
153
|
+
by: string;
|
|
154
|
+
eventType: string;
|
|
155
|
+
fingerprint: string; // pre-computed sha256 of content
|
|
156
|
+
}): Promise<{ snapshot: Snapshot; blocks: Block[] }> {
|
|
157
|
+
const { rootDir, mdPath, content, sidecar, by, eventType, fingerprint } = args;
|
|
158
|
+
const nodes = parseBlocks(content);
|
|
159
|
+
const { blocks, newRefMap } = assignRefs(nodes, sidecar);
|
|
160
|
+
const oldFingerprint = sidecar.fingerprint;
|
|
161
|
+
sidecar.refMap = newRefMap;
|
|
162
|
+
sidecar.revision += 1;
|
|
163
|
+
sidecar.updatedAt = nowIso();
|
|
164
|
+
sidecar.fingerprint = fingerprint;
|
|
165
|
+
markOrphanedRefsStale(sidecar, newRefMap);
|
|
166
|
+
const eventPayload: Omit<ProofEvent, "id"> & Record<string, unknown> = {
|
|
167
|
+
type: eventType,
|
|
168
|
+
at: nowIso(),
|
|
169
|
+
by,
|
|
170
|
+
fingerprint,
|
|
171
|
+
};
|
|
172
|
+
if (eventType === "file.rawWritten") {
|
|
173
|
+
eventPayload.oldSha = oldFingerprint;
|
|
174
|
+
eventPayload.newSha = fingerprint;
|
|
175
|
+
}
|
|
176
|
+
emitEvents(sidecar, [eventPayload as Omit<ProofEvent, "id">]);
|
|
177
|
+
await writeSidecar(rootDir, mdPath, sidecar);
|
|
178
|
+
return { snapshot: buildSnapshot(mdPath, blocks, sidecar), blocks };
|
|
179
|
+
}
|
|
180
|
+
|
|
124
181
|
/**
|
|
125
182
|
* Pure read: load file, parse blocks, assign refs, return snapshot.
|
|
126
183
|
* Detects external edits (fingerprint mismatch) and records them in the sidecar.
|
|
@@ -156,20 +213,16 @@ export async function readSnapshot(
|
|
|
156
213
|
const freshFingerprint = sha256file(freshContent);
|
|
157
214
|
|
|
158
215
|
if (freshSidecar.fingerprint && freshSidecar.fingerprint !== freshFingerprint) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
freshSidecar.fingerprint = freshFingerprint;
|
|
165
|
-
emitEvents(freshSidecar, [{
|
|
166
|
-
type: "file.externallyEdited",
|
|
167
|
-
at: nowIso(),
|
|
216
|
+
const { snapshot } = await reconcileSidecar({
|
|
217
|
+
rootDir,
|
|
218
|
+
mdPath,
|
|
219
|
+
content: freshContent,
|
|
220
|
+
sidecar: freshSidecar,
|
|
168
221
|
by: "system",
|
|
222
|
+
eventType: "file.externallyEdited",
|
|
169
223
|
fingerprint: freshFingerprint,
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return buildSnapshot(mdPath, freshBlocks, freshSidecar);
|
|
224
|
+
});
|
|
225
|
+
return snapshot;
|
|
173
226
|
}
|
|
174
227
|
|
|
175
228
|
// Another writer already updated — just build snapshot from current state
|
|
@@ -233,28 +286,24 @@ export async function applyOps(args: {
|
|
|
233
286
|
|
|
234
287
|
const isAi = by.startsWith("ai:");
|
|
235
288
|
|
|
236
|
-
// Detect external edits
|
|
289
|
+
// Detect external edits — reconcile eagerly inside the mutex (R2: do not let lazy reconcile miss)
|
|
237
290
|
if (sidecar.fingerprint && sidecar.fingerprint !== fingerprint) {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
sidecar.fingerprint = fingerprint;
|
|
244
|
-
emitEvents(sidecar, [{
|
|
245
|
-
type: "file.externallyEdited",
|
|
246
|
-
at: nowIso(),
|
|
291
|
+
const { snapshot: freshSnapshot } = await reconcileSidecar({
|
|
292
|
+
rootDir,
|
|
293
|
+
mdPath,
|
|
294
|
+
content,
|
|
295
|
+
sidecar,
|
|
247
296
|
by: "system",
|
|
297
|
+
eventType: "file.externallyEdited",
|
|
248
298
|
fingerprint,
|
|
249
|
-
}
|
|
250
|
-
await writeSidecar(rootDir, mdPath, sidecar);
|
|
299
|
+
});
|
|
251
300
|
// Return STALE_REVISION so caller knows to re-fetch
|
|
252
301
|
return {
|
|
253
302
|
ok: false,
|
|
254
303
|
status: 409,
|
|
255
304
|
code: "STALE_REVISION",
|
|
256
305
|
message: "File was externally edited. Fetch the new snapshot and retry.",
|
|
257
|
-
snapshot:
|
|
306
|
+
snapshot: freshSnapshot,
|
|
258
307
|
};
|
|
259
308
|
}
|
|
260
309
|
|