tgo-wiki 0.1.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/CHANGELOG.md +32 -0
- package/README.md +255 -0
- package/docs/mcp-usage.md +631 -0
- package/docs/v0-acceptance.md +105 -0
- package/docs/v0-delivery-checklist.md +57 -0
- package/docs/v1-acceptance.md +39 -0
- package/docs/v2-acceptance.md +165 -0
- package/package.json +69 -0
- package/packages/core/src/config/config-loader.ts +109 -0
- package/packages/core/src/config/defaults.ts +74 -0
- package/packages/core/src/config/workspace-resolver.ts +40 -0
- package/packages/core/src/documents/command-document-parser.ts +206 -0
- package/packages/core/src/documents/document-id.ts +26 -0
- package/packages/core/src/documents/document-parser-registry.ts +126 -0
- package/packages/core/src/documents/document-service.ts +656 -0
- package/packages/core/src/documents/document-store.ts +132 -0
- package/packages/core/src/documents/document-types.ts +33 -0
- package/packages/core/src/documents/pdf-text-parser.ts +35 -0
- package/packages/core/src/documents/text-markdown-parser.ts +50 -0
- package/packages/core/src/errors.ts +46 -0
- package/packages/core/src/git/git-service.ts +68 -0
- package/packages/core/src/index.ts +38 -0
- package/packages/core/src/markdown/markdown-scanner.ts +90 -0
- package/packages/core/src/permissions/permission-service.ts +50 -0
- package/packages/core/src/publish/publish-service.ts +142 -0
- package/packages/core/src/result.ts +13 -0
- package/packages/core/src/services/session-workflow-service.ts +493 -0
- package/packages/core/src/services/wiki-service.ts +119 -0
- package/packages/core/src/services/workspace-service.ts +223 -0
- package/packages/core/src/session/session-id.ts +14 -0
- package/packages/core/src/session/session-service.ts +77 -0
- package/packages/core/src/session/session-store.ts +91 -0
- package/packages/core/src/session/session-types.ts +17 -0
- package/packages/core/src/sources/source-id.ts +19 -0
- package/packages/core/src/sources/source-paths.ts +15 -0
- package/packages/core/src/sources/source-service.ts +416 -0
- package/packages/core/src/sources/source-types.ts +77 -0
- package/packages/core/src/sources/source-validator.ts +132 -0
- package/packages/core/src/sources/source-writer.ts +419 -0
- package/packages/core/src/validation/frontmatter-validator.ts +128 -0
- package/packages/core/src/validation/link-validator.ts +55 -0
- package/packages/core/src/validation/path-validator.ts +65 -0
- package/packages/core/src/validation/source-reference-validator.ts +191 -0
- package/packages/core/src/validation/validation-service.ts +106 -0
- package/packages/core/src/vfs/vfs-command-parser.ts +69 -0
- package/packages/core/src/vfs/vfs-service.ts +498 -0
- package/packages/core/src/web/html-to-markdown.ts +144 -0
- package/packages/core/src/web/static-web-fetcher.ts +537 -0
- package/packages/core/src/web/web-id.ts +26 -0
- package/packages/core/src/web/web-ingestion-service.ts +335 -0
- package/packages/core/src/web/web-paths.ts +6 -0
- package/packages/core/src/web/web-types.ts +33 -0
- package/packages/server/src/cli.ts +56 -0
- package/packages/server/src/context.ts +7 -0
- package/packages/server/src/index.ts +2 -0
- package/packages/server/src/mcp-server.ts +111 -0
- package/packages/server/src/schemas/documents.ts +17 -0
- package/packages/server/src/schemas/read.ts +16 -0
- package/packages/server/src/schemas/session.ts +31 -0
- package/packages/server/src/schemas/sources.ts +12 -0
- package/packages/server/src/schemas/web.ts +23 -0
- package/packages/server/src/tools/document-tools.ts +46 -0
- package/packages/server/src/tools/publish-tools.ts +33 -0
- package/packages/server/src/tools/read-tools.ts +52 -0
- package/packages/server/src/tools/response.ts +24 -0
- package/packages/server/src/tools/session-tools.ts +100 -0
- package/packages/server/src/tools/source-tools.ts +32 -0
- package/packages/server/src/tools/web-tools.ts +26 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { defaultConfig } from "../config/defaults.js";
|
|
4
|
+
import { resolveWorkspacePaths, type WorkspacePaths } from "../config/workspace-resolver.js";
|
|
5
|
+
import { toWikiError } from "../errors.js";
|
|
6
|
+
import { GitService } from "../git/git-service.js";
|
|
7
|
+
import { err, ok, type Result } from "../result.js";
|
|
8
|
+
|
|
9
|
+
export type WorkspaceInitResult = {
|
|
10
|
+
workspaceRoot: string;
|
|
11
|
+
stableCommit: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type WorkspaceStatus = {
|
|
15
|
+
workspaceRoot: string;
|
|
16
|
+
repoPath: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
commit: string;
|
|
19
|
+
worktree: string;
|
|
20
|
+
dirty: boolean;
|
|
21
|
+
sessionCount: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const initialIndexPage = `---
|
|
25
|
+
title: "Home"
|
|
26
|
+
summary: "Entry point for the wiki."
|
|
27
|
+
tags: ["home"]
|
|
28
|
+
updated: "2026-06-11"
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Home
|
|
32
|
+
|
|
33
|
+
Welcome to the wiki.
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export class WorkspaceService {
|
|
37
|
+
private readonly paths: WorkspacePaths;
|
|
38
|
+
private readonly git: GitService;
|
|
39
|
+
|
|
40
|
+
constructor(workspaceRoot: string, git = new GitService()) {
|
|
41
|
+
this.paths = resolveWorkspacePaths(workspaceRoot);
|
|
42
|
+
this.git = git;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async init(): Promise<Result<WorkspaceInitResult>> {
|
|
46
|
+
try {
|
|
47
|
+
await this.ensureWorkspaceDirs();
|
|
48
|
+
await this.ensureConfig();
|
|
49
|
+
await this.ensureRepo();
|
|
50
|
+
const stableCommit = await this.ensureStableBranch();
|
|
51
|
+
await this.ensureStableWorktree();
|
|
52
|
+
await this.ensureStableWorktreeReadable();
|
|
53
|
+
|
|
54
|
+
return ok({
|
|
55
|
+
workspaceRoot: this.paths.workspaceRoot,
|
|
56
|
+
stableCommit
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return err(toWikiError(error, "git_error"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async status(): Promise<Result<WorkspaceStatus>> {
|
|
64
|
+
try {
|
|
65
|
+
const commit = (await this.git.run(["rev-parse", defaultConfig.stableBranch], { cwd: this.paths.repoPath })).stdout.trim();
|
|
66
|
+
const porcelain = (
|
|
67
|
+
await this.git.run(["status", "--porcelain"], { cwd: this.paths.stableWorktreePath })
|
|
68
|
+
).stdout.trim();
|
|
69
|
+
const sessions = await this.sessionCount();
|
|
70
|
+
|
|
71
|
+
return ok({
|
|
72
|
+
workspaceRoot: this.paths.workspaceRoot,
|
|
73
|
+
repoPath: this.paths.repoPath,
|
|
74
|
+
branch: defaultConfig.stableBranch,
|
|
75
|
+
commit,
|
|
76
|
+
worktree: this.paths.stableWorktreePath,
|
|
77
|
+
dirty: porcelain.length > 0,
|
|
78
|
+
sessionCount: sessions
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return err(toWikiError(error, "git_error"));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async ensureWorkspaceDirs(): Promise<void> {
|
|
86
|
+
await Promise.all([
|
|
87
|
+
mkdir(this.paths.repoPath, { recursive: true }),
|
|
88
|
+
mkdir(this.paths.statePath, { recursive: true }),
|
|
89
|
+
mkdir(this.paths.sessionsStatePath, { recursive: true }),
|
|
90
|
+
mkdir(this.paths.documentsStatePath, { recursive: true }),
|
|
91
|
+
mkdir(this.paths.documentBlobsPath, { recursive: true }),
|
|
92
|
+
mkdir(this.paths.pendingDocumentsPath, { recursive: true }),
|
|
93
|
+
mkdir(this.paths.worktreesPath, { recursive: true }),
|
|
94
|
+
mkdir(this.paths.sessionsWorktreePath, { recursive: true })
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async ensureConfig(): Promise<void> {
|
|
99
|
+
const configPath = path.join(this.paths.statePath, "config.json");
|
|
100
|
+
|
|
101
|
+
if (await exists(configPath)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf8");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async ensureRepo(): Promise<void> {
|
|
109
|
+
if (!(await exists(path.join(this.paths.repoPath, ".git")))) {
|
|
110
|
+
await this.git.run(["init"], { cwd: this.paths.repoPath });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!(await this.hasCommits())) {
|
|
114
|
+
await this.git.run(["symbolic-ref", "HEAD", `refs/heads/${defaultConfig.repoManagementBranch}`], {
|
|
115
|
+
cwd: this.paths.repoPath
|
|
116
|
+
});
|
|
117
|
+
await this.writeInitialPage();
|
|
118
|
+
await this.git.commit(this.paths.repoPath, "chore: initialize wiki");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!(await this.branchExists(defaultConfig.repoManagementBranch))) {
|
|
123
|
+
await this.git.run(["branch", defaultConfig.repoManagementBranch, "HEAD"], { cwd: this.paths.repoPath });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.git.run(["checkout", defaultConfig.repoManagementBranch], { cwd: this.paths.repoPath });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async writeInitialPage(): Promise<void> {
|
|
130
|
+
const wikiPath = path.join(this.paths.repoPath, this.paths.wikiRootName);
|
|
131
|
+
await mkdir(wikiPath, { recursive: true });
|
|
132
|
+
await writeFile(path.join(wikiPath, "index.md"), initialIndexPage, "utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async ensureStableBranch(): Promise<string> {
|
|
136
|
+
const head = (await this.git.run(["rev-parse", "HEAD"], { cwd: this.paths.repoPath })).stdout.trim();
|
|
137
|
+
|
|
138
|
+
if (!(await this.branchExists(defaultConfig.stableBranch))) {
|
|
139
|
+
await this.git.run(["branch", defaultConfig.stableBranch, head], { cwd: this.paths.repoPath });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (await this.git.run(["rev-parse", defaultConfig.stableBranch], { cwd: this.paths.repoPath })).stdout.trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async ensureStableWorktree(): Promise<void> {
|
|
146
|
+
if (await this.hasWorktree(this.paths.stableWorktreePath)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await this.git.run(["worktree", "add", this.paths.stableWorktreePath, defaultConfig.stableBranch], {
|
|
151
|
+
cwd: this.paths.repoPath
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async ensureStableWorktreeReadable(): Promise<void> {
|
|
156
|
+
await readFile(path.join(this.paths.stableWorktreePath, this.paths.wikiRootName, "index.md"), "utf8");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async hasCommits(): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
await this.git.run(["rev-parse", "--verify", "HEAD"], { cwd: this.paths.repoPath });
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async branchExists(branch: string): Promise<boolean> {
|
|
169
|
+
try {
|
|
170
|
+
await this.git.run(["rev-parse", "--verify", `refs/heads/${branch}`], { cwd: this.paths.repoPath });
|
|
171
|
+
return true;
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async hasWorktree(worktreePath: string): Promise<boolean> {
|
|
178
|
+
const expectedPath = await canonicalPath(worktreePath);
|
|
179
|
+
const list = await this.git.run(["worktree", "list", "--porcelain"], { cwd: this.paths.repoPath });
|
|
180
|
+
const worktrees = list.stdout
|
|
181
|
+
.split("\n")
|
|
182
|
+
.filter(line => line.startsWith("worktree "))
|
|
183
|
+
.map(line => line.slice("worktree ".length));
|
|
184
|
+
|
|
185
|
+
for (const worktree of worktrees) {
|
|
186
|
+
if ((await canonicalPath(worktree)) === expectedPath) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async sessionCount(): Promise<number> {
|
|
195
|
+
try {
|
|
196
|
+
const entries = await readdir(this.paths.sessionsStatePath, { withFileTypes: true });
|
|
197
|
+
return entries.filter(entry => entry.isFile() && entry.name.endsWith(".json")).length;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function exists(filePath: string): Promise<boolean> {
|
|
209
|
+
try {
|
|
210
|
+
await access(filePath);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function canonicalPath(filePath: string): Promise<string> {
|
|
218
|
+
try {
|
|
219
|
+
return await realpath(filePath);
|
|
220
|
+
} catch {
|
|
221
|
+
return path.resolve(filePath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { WikiError } from "../errors.js";
|
|
3
|
+
|
|
4
|
+
export function generateSessionId(now = new Date()): string {
|
|
5
|
+
const date = now.toISOString().slice(0, 10).replaceAll("-", "");
|
|
6
|
+
const suffix = randomBytes(4).toString("hex");
|
|
7
|
+
return `${date}-${suffix}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function assertValidSessionId(sessionId: string): void {
|
|
11
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(sessionId)) {
|
|
12
|
+
throw new WikiError("session_not_found", `Invalid session id: ${sessionId}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defaultConfig } from "../config/defaults.js";
|
|
3
|
+
import { resolveWorkspacePaths, type WorkspacePaths } from "../config/workspace-resolver.js";
|
|
4
|
+
import { toWikiError } from "../errors.js";
|
|
5
|
+
import { GitService } from "../git/git-service.js";
|
|
6
|
+
import { err, ok, type Result } from "../result.js";
|
|
7
|
+
import { assertValidSessionId, generateSessionId } from "./session-id.js";
|
|
8
|
+
import { SessionStore } from "./session-store.js";
|
|
9
|
+
import type { SessionMetadata } from "./session-types.js";
|
|
10
|
+
|
|
11
|
+
export type SessionStartInput = {
|
|
12
|
+
baseRef?: "stable" | string;
|
|
13
|
+
purpose?: string;
|
|
14
|
+
agentId?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SessionStartResult = {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
branch: string;
|
|
20
|
+
worktree: string;
|
|
21
|
+
baseRef: string;
|
|
22
|
+
baseCommit: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class SessionService {
|
|
26
|
+
private readonly paths: WorkspacePaths;
|
|
27
|
+
private readonly store: SessionStore;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
workspaceRoot: string,
|
|
31
|
+
private readonly idGenerator = generateSessionId,
|
|
32
|
+
private readonly git = new GitService()
|
|
33
|
+
) {
|
|
34
|
+
this.paths = resolveWorkspacePaths(workspaceRoot);
|
|
35
|
+
this.store = new SessionStore(this.paths);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start(input: SessionStartInput = {}): Promise<Result<SessionStartResult>> {
|
|
39
|
+
try {
|
|
40
|
+
const baseRef = input.baseRef ?? "stable";
|
|
41
|
+
const gitBaseRef = baseRef === "stable" ? defaultConfig.stableBranch : baseRef;
|
|
42
|
+
const baseCommit = (await this.git.run(["rev-parse", gitBaseRef], { cwd: this.paths.repoPath })).stdout.trim();
|
|
43
|
+
const sessionId = this.idGenerator();
|
|
44
|
+
assertValidSessionId(sessionId);
|
|
45
|
+
|
|
46
|
+
const branch = `wiki/session/${sessionId}`;
|
|
47
|
+
const worktree = path.join(this.paths.sessionsWorktreePath, sessionId);
|
|
48
|
+
|
|
49
|
+
await this.git.run(["worktree", "add", "-b", branch, worktree, baseCommit], { cwd: this.paths.repoPath });
|
|
50
|
+
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
const metadata: SessionMetadata = {
|
|
53
|
+
sessionId,
|
|
54
|
+
agentId: input.agentId,
|
|
55
|
+
purpose: input.purpose,
|
|
56
|
+
baseRef,
|
|
57
|
+
baseCommit,
|
|
58
|
+
branch,
|
|
59
|
+
worktree: path.relative(this.paths.workspaceRoot, worktree),
|
|
60
|
+
createdAt: now,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
status: "open"
|
|
63
|
+
};
|
|
64
|
+
await this.store.write(metadata);
|
|
65
|
+
|
|
66
|
+
return ok({
|
|
67
|
+
sessionId,
|
|
68
|
+
branch,
|
|
69
|
+
worktree,
|
|
70
|
+
baseRef,
|
|
71
|
+
baseCommit
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return err(toWikiError(error, "git_error"));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { WorkspacePaths } from "../config/workspace-resolver.js";
|
|
4
|
+
import { WikiError } from "../errors.js";
|
|
5
|
+
import { assertValidSessionId } from "./session-id.js";
|
|
6
|
+
import type { SessionMetadata, SessionStatus } from "./session-types.js";
|
|
7
|
+
|
|
8
|
+
export class SessionStore {
|
|
9
|
+
constructor(private readonly paths: WorkspacePaths) {}
|
|
10
|
+
|
|
11
|
+
async read(sessionId: string): Promise<SessionMetadata> {
|
|
12
|
+
assertValidSessionId(sessionId);
|
|
13
|
+
|
|
14
|
+
let raw: string;
|
|
15
|
+
try {
|
|
16
|
+
raw = await readFile(this.metadataPath(sessionId), "utf8");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new WikiError("session_not_found", `Session metadata not found: ${sessionId}`, {
|
|
19
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let metadata: unknown;
|
|
24
|
+
try {
|
|
25
|
+
metadata = JSON.parse(raw);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new WikiError("session_metadata_invalid", `Session metadata is malformed JSON: ${sessionId}`, {
|
|
28
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!isSessionMetadata(metadata, sessionId)) {
|
|
33
|
+
throw new WikiError("session_metadata_invalid", `Session metadata is invalid: ${sessionId}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return metadata;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async write(metadata: SessionMetadata): Promise<void> {
|
|
40
|
+
assertValidSessionId(metadata.sessionId);
|
|
41
|
+
await mkdir(this.paths.sessionsStatePath, { recursive: true });
|
|
42
|
+
await writeFile(this.metadataPath(metadata.sessionId), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async update(sessionId: string, update: (metadata: SessionMetadata) => SessionMetadata): Promise<SessionMetadata> {
|
|
46
|
+
const current = await this.read(sessionId);
|
|
47
|
+
const next = update(current);
|
|
48
|
+
await this.write(next);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
metadataPath(sessionId: string): string {
|
|
53
|
+
assertValidSessionId(sessionId);
|
|
54
|
+
return path.join(this.paths.sessionsStatePath, `${sessionId}.json`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isSessionMetadata(value: unknown, sessionId: string): value is SessionMetadata {
|
|
59
|
+
if (!value || typeof value !== "object") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const metadata = value as Partial<SessionMetadata>;
|
|
64
|
+
return (
|
|
65
|
+
metadata.sessionId === sessionId &&
|
|
66
|
+
isString(metadata.baseRef) &&
|
|
67
|
+
isString(metadata.baseCommit) &&
|
|
68
|
+
isString(metadata.branch) &&
|
|
69
|
+
isString(metadata.worktree) &&
|
|
70
|
+
isString(metadata.createdAt) &&
|
|
71
|
+
isString(metadata.updatedAt) &&
|
|
72
|
+
isSessionStatus(metadata.status) &&
|
|
73
|
+
isOptionalString(metadata.agentId) &&
|
|
74
|
+
isOptionalString(metadata.purpose) &&
|
|
75
|
+
isOptionalString(metadata.lastCommit) &&
|
|
76
|
+
isOptionalString(metadata.publishedAt) &&
|
|
77
|
+
isOptionalString(metadata.publishedCommit)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isSessionStatus(value: unknown): value is SessionStatus {
|
|
82
|
+
return value === "open" || value === "published" || value === "abandoned";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isString(value: unknown): value is string {
|
|
86
|
+
return typeof value === "string";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isOptionalString(value: unknown): value is string | undefined {
|
|
90
|
+
return value === undefined || typeof value === "string";
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SessionStatus = "open" | "published" | "abandoned";
|
|
2
|
+
|
|
3
|
+
export type SessionMetadata = {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
agentId?: string;
|
|
6
|
+
purpose?: string;
|
|
7
|
+
baseRef: string;
|
|
8
|
+
baseCommit: string;
|
|
9
|
+
branch: string;
|
|
10
|
+
worktree: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
status: SessionStatus;
|
|
14
|
+
lastCommit?: string;
|
|
15
|
+
publishedAt?: string;
|
|
16
|
+
publishedCommit?: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { WikiError } from "../errors.js";
|
|
3
|
+
|
|
4
|
+
const sourceIdPattern = /^[a-zA-Z0-9._-]+$/;
|
|
5
|
+
|
|
6
|
+
export function assertValidSourceId(id: string): void {
|
|
7
|
+
if (
|
|
8
|
+
id.length === 0 ||
|
|
9
|
+
!sourceIdPattern.test(id) ||
|
|
10
|
+
id.includes("/") ||
|
|
11
|
+
id.includes("\\") ||
|
|
12
|
+
id === "." ||
|
|
13
|
+
id === ".." ||
|
|
14
|
+
path.isAbsolute(id) ||
|
|
15
|
+
path.win32.isAbsolute(id)
|
|
16
|
+
) {
|
|
17
|
+
throw new WikiError("invalid_path", `Invalid source id: ${id}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertValidSourceId } from "./source-id.js";
|
|
3
|
+
|
|
4
|
+
export function sourceDirectory(worktreeRoot: string, id: string): string {
|
|
5
|
+
assertValidSourceId(id);
|
|
6
|
+
return path.join(worktreeRoot, "sources", id);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function sourceMetadataPath(worktreeRoot: string, id: string): string {
|
|
10
|
+
return path.join(sourceDirectory(worktreeRoot, id), "metadata.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sourceRawMarkdownPath(worktreeRoot: string, id: string): string {
|
|
14
|
+
return path.join(sourceDirectory(worktreeRoot, id), "raw.md");
|
|
15
|
+
}
|