chapterhouse 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +363 -0
  3. package/agents/chapterhouse.agent.md +40 -0
  4. package/agents/coder.agent.md +38 -0
  5. package/agents/designer.agent.md +43 -0
  6. package/agents/general-purpose.agent.md +30 -0
  7. package/dist/api/auth.js +159 -0
  8. package/dist/api/auth.test.js +463 -0
  9. package/dist/api/errors.js +95 -0
  10. package/dist/api/errors.test.js +89 -0
  11. package/dist/api/rate-limit.js +85 -0
  12. package/dist/api/server-runtime.js +62 -0
  13. package/dist/api/server.js +651 -0
  14. package/dist/api/server.test.js +385 -0
  15. package/dist/api/sse.integration.test.js +270 -0
  16. package/dist/api/sse.js +7 -0
  17. package/dist/api/team.js +196 -0
  18. package/dist/api/team.test.js +466 -0
  19. package/dist/cli.js +102 -0
  20. package/dist/config.js +299 -0
  21. package/dist/config.phase3.test.js +20 -0
  22. package/dist/config.test.js +148 -0
  23. package/dist/copilot/agents.js +447 -0
  24. package/dist/copilot/agents.squad.test.js +72 -0
  25. package/dist/copilot/classifier.js +72 -0
  26. package/dist/copilot/client.js +32 -0
  27. package/dist/copilot/client.test.js +100 -0
  28. package/dist/copilot/episode-writer.js +219 -0
  29. package/dist/copilot/episode-writer.test.js +41 -0
  30. package/dist/copilot/mcp-config.js +22 -0
  31. package/dist/copilot/okr-mapper.js +196 -0
  32. package/dist/copilot/okr-mapper.test.js +114 -0
  33. package/dist/copilot/orchestrator.js +685 -0
  34. package/dist/copilot/orchestrator.test.js +523 -0
  35. package/dist/copilot/router.js +142 -0
  36. package/dist/copilot/router.test.js +119 -0
  37. package/dist/copilot/skills.js +125 -0
  38. package/dist/copilot/standup.js +138 -0
  39. package/dist/copilot/standup.test.js +132 -0
  40. package/dist/copilot/system-message.js +143 -0
  41. package/dist/copilot/system-message.test.js +17 -0
  42. package/dist/copilot/tools.js +1212 -0
  43. package/dist/copilot/tools.okr.test.js +260 -0
  44. package/dist/copilot/tools.squad.test.js +168 -0
  45. package/dist/daemon.js +235 -0
  46. package/dist/home-path.js +12 -0
  47. package/dist/home-path.test.js +11 -0
  48. package/dist/integrations/ado-analytics.js +178 -0
  49. package/dist/integrations/ado-analytics.test.js +284 -0
  50. package/dist/integrations/ado-client.js +227 -0
  51. package/dist/integrations/ado-client.test.js +176 -0
  52. package/dist/integrations/ado-schema.js +25 -0
  53. package/dist/integrations/ado-schema.test.js +39 -0
  54. package/dist/integrations/ado-skill.js +55 -0
  55. package/dist/integrations/report-generator.js +114 -0
  56. package/dist/integrations/report-generator.test.js +62 -0
  57. package/dist/integrations/team-push.js +144 -0
  58. package/dist/integrations/team-push.test.js +178 -0
  59. package/dist/integrations/teams-notify.js +108 -0
  60. package/dist/integrations/teams-notify.test.js +135 -0
  61. package/dist/paths.js +41 -0
  62. package/dist/setup.js +149 -0
  63. package/dist/shutdown-signals.js +13 -0
  64. package/dist/shutdown-signals.test.js +33 -0
  65. package/dist/squad/charter.js +108 -0
  66. package/dist/squad/charter.test.js +89 -0
  67. package/dist/squad/context.js +48 -0
  68. package/dist/squad/context.test.js +59 -0
  69. package/dist/squad/discovery.js +280 -0
  70. package/dist/squad/discovery.test.js +93 -0
  71. package/dist/squad/index.js +7 -0
  72. package/dist/squad/mirror.js +81 -0
  73. package/dist/squad/mirror.scheduler.js +78 -0
  74. package/dist/squad/mirror.scheduler.test.js +197 -0
  75. package/dist/squad/mirror.test.js +172 -0
  76. package/dist/squad/registry.js +162 -0
  77. package/dist/squad/registry.test.js +31 -0
  78. package/dist/squad/squad-coordinator-system-message.test.js +190 -0
  79. package/dist/squad/squad-session-routing.test.js +260 -0
  80. package/dist/squad/types.js +4 -0
  81. package/dist/status.js +25 -0
  82. package/dist/status.test.js +22 -0
  83. package/dist/store/db.js +290 -0
  84. package/dist/store/db.test.js +126 -0
  85. package/dist/store/squad-sessions.test.js +341 -0
  86. package/dist/test/setup-env.js +3 -0
  87. package/dist/update.js +112 -0
  88. package/dist/update.test.js +25 -0
  89. package/dist/wiki/context.js +138 -0
  90. package/dist/wiki/fs.js +195 -0
  91. package/dist/wiki/fs.test.js +39 -0
  92. package/dist/wiki/index-manager.js +359 -0
  93. package/dist/wiki/index-manager.test.js +129 -0
  94. package/dist/wiki/lock.js +26 -0
  95. package/dist/wiki/lock.test.js +30 -0
  96. package/dist/wiki/log-manager.js +20 -0
  97. package/dist/wiki/migrate.js +306 -0
  98. package/dist/wiki/okr.test.js +101 -0
  99. package/dist/wiki/path-utils.js +4 -0
  100. package/dist/wiki/path-utils.test.js +8 -0
  101. package/dist/wiki/seed-team-wiki.js +296 -0
  102. package/dist/wiki/seed-team-wiki.test.js +69 -0
  103. package/dist/wiki/team-sync.js +212 -0
  104. package/dist/wiki/team-sync.test.js +185 -0
  105. package/dist/wiki/templates/okr.js +98 -0
  106. package/package.json +72 -0
  107. package/skills/.gitkeep +0 -0
  108. package/skills/find-skills/SKILL.md +161 -0
  109. package/skills/find-skills/_meta.json +4 -0
  110. package/skills/frontend-design/LICENSE.txt +177 -0
  111. package/skills/frontend-design/SKILL.md +42 -0
  112. package/skills/squad/SKILL.md +76 -0
  113. package/web/dist/assets/index-D-e7K-fT.css +10 -0
  114. package/web/dist/assets/index-DAg9IrpO.js +142 -0
  115. package/web/dist/assets/index-DAg9IrpO.js.map +1 -0
  116. package/web/dist/chapterhouse-icon.png +0 -0
  117. package/web/dist/chapterhouse-icon.svg +42 -0
  118. package/web/dist/chapterhouse-logo.svg +46 -0
  119. package/web/dist/index.html +15 -0
@@ -0,0 +1,296 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { resolve } from "node:path";
3
+ import { ensureWikiStructure, readPage, writePage } from "./fs.js";
4
+ import { addToIndex, buildIndexEntryForPage } from "./index-manager.js";
5
+ import { generateKPIPage, generateOKRQuarterPage, generateTeamIndexPage, } from "./templates/okr.js";
6
+ const SAMPLE_OBJECTIVES = [
7
+ {
8
+ id: "O1",
9
+ title: "Increase Azure platform deployment reliability across regulated regions",
10
+ owner: "Ava Wilson",
11
+ keyResults: [
12
+ {
13
+ id: "O1-KR1",
14
+ title: "Lift first-pass success for regional rollout pipelines from 72 to 95 percent",
15
+ owner: "Noah Patel",
16
+ targetValue: 95,
17
+ currentValue: 72,
18
+ unit: "%",
19
+ dueDate: "2026-06-30",
20
+ },
21
+ {
22
+ id: "O1-KR2",
23
+ title: "Cut mean time to restore failed environment promotions from 180 to 45 minutes",
24
+ owner: "Maya Chen",
25
+ targetValue: 45,
26
+ currentValue: 180,
27
+ unit: "minutes",
28
+ dueDate: "2026-06-15",
29
+ },
30
+ {
31
+ id: "O1-KR3",
32
+ title: "Automate compliance evidence capture for 100 percent of production releases",
33
+ owner: "Jordan Alvarez",
34
+ targetValue: 100,
35
+ currentValue: 20,
36
+ unit: "%",
37
+ dueDate: "2026-06-30",
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ id: "O2",
43
+ title: "Improve Azure service onboarding velocity for internal product teams",
44
+ owner: "Lena Brooks",
45
+ keyResults: [
46
+ {
47
+ id: "O2-KR1",
48
+ title: "Reduce median landing zone provisioning time from 14 days to 5 days",
49
+ owner: "Priya Raman",
50
+ targetValue: 5,
51
+ currentValue: 14,
52
+ unit: "days",
53
+ dueDate: "2026-06-30",
54
+ },
55
+ {
56
+ id: "O2-KR2",
57
+ title: "Publish reusable Terraform blueprints covering 8 common Azure workloads",
58
+ owner: "Diego Martinez",
59
+ targetValue: 8,
60
+ currentValue: 2,
61
+ unit: "blueprints",
62
+ dueDate: "2026-06-20",
63
+ },
64
+ {
65
+ id: "O2-KR3",
66
+ title: "Raise onboarding satisfaction from 3.6 to 4.5 out of 5 in quarterly surveys",
67
+ owner: "Sofia Kim",
68
+ targetValue: 4.5,
69
+ currentValue: 3.6,
70
+ unit: "score",
71
+ dueDate: "2026-06-30",
72
+ },
73
+ ],
74
+ },
75
+ ];
76
+ const SAMPLE_KPIS = [
77
+ {
78
+ id: "kpi-release-cadence",
79
+ name: "Production release cadence",
80
+ owner: "Ava Wilson",
81
+ target: 12,
82
+ current: 9,
83
+ unit: "releases",
84
+ frequency: "monthly",
85
+ },
86
+ {
87
+ id: "kpi-change-failure-rate",
88
+ name: "Change failure rate",
89
+ owner: "Noah Patel",
90
+ target: 5,
91
+ current: 8,
92
+ unit: "%",
93
+ frequency: "weekly",
94
+ },
95
+ {
96
+ id: "kpi-sev1-sev2-incidents",
97
+ name: "Sev1/Sev2 incidents",
98
+ owner: "Maya Chen",
99
+ target: 2,
100
+ current: 4,
101
+ unit: "count",
102
+ frequency: "monthly",
103
+ },
104
+ {
105
+ id: "kpi-infra-cost-variance",
106
+ name: "Azure spend variance to forecast",
107
+ owner: "Jordan Alvarez",
108
+ target: 3,
109
+ current: 6,
110
+ unit: "%",
111
+ frequency: "monthly",
112
+ },
113
+ {
114
+ id: "kpi-service-onboarding-time",
115
+ name: "Median service onboarding time",
116
+ owner: "Lena Brooks",
117
+ target: 5,
118
+ current: 11,
119
+ unit: "days",
120
+ frequency: "quarterly",
121
+ },
122
+ ];
123
+ const PLACEHOLDER_MEMBERS = [
124
+ {
125
+ name: "TBD Engineer 1",
126
+ email: "engineer1@chapterhouse.example",
127
+ role: "team-lead",
128
+ entraObjectId: "00000000-0000-0000-0000-000000000001",
129
+ okrOwnership: ["O1-KR1", "O2-KR1"],
130
+ },
131
+ {
132
+ name: "TBD Engineer 2",
133
+ email: "engineer2@chapterhouse.example",
134
+ role: "engineer",
135
+ entraObjectId: "00000000-0000-0000-0000-000000000002",
136
+ okrOwnership: ["O1-KR2"],
137
+ },
138
+ {
139
+ name: "TBD Engineer 3",
140
+ email: "engineer3@chapterhouse.example",
141
+ role: "engineer",
142
+ entraObjectId: "00000000-0000-0000-0000-000000000003",
143
+ okrOwnership: ["O1-KR3"],
144
+ },
145
+ {
146
+ name: "TBD Engineer 4",
147
+ email: "engineer4@chapterhouse.example",
148
+ role: "engineer",
149
+ entraObjectId: "00000000-0000-0000-0000-000000000004",
150
+ okrOwnership: ["O2-KR2"],
151
+ },
152
+ {
153
+ name: "TBD Engineer 5",
154
+ email: "engineer5@chapterhouse.example",
155
+ role: "engineer",
156
+ entraObjectId: "00000000-0000-0000-0000-000000000005",
157
+ okrOwnership: ["O2-KR3"],
158
+ },
159
+ ];
160
+ function generateOnboardingPage() {
161
+ return [
162
+ "# Team Onboarding",
163
+ "",
164
+ "Welcome to the Team Chapterhouse wiki. This space is the authoritative team source for OKRs, KPIs, and operating notes that sync down to personal Chapterhouse instances.",
165
+ "",
166
+ "## Get oriented",
167
+ "1. Review `pages/okrs/2026-Q2.md` to understand the current quarterly objectives and the key results you own.",
168
+ "2. Review `pages/kpis/team.md` to see the operational metrics the team uses for weekly and monthly health checks.",
169
+ "3. Review `pages/team/index.md` and replace the placeholder roster with the current team membership and KR ownership.",
170
+ "",
171
+ "## Log OKR updates",
172
+ "1. Add daily or milestone progress through Chapterhouse so updates land in `pages/okrs/updates/YYYY-MM-DD.md`.",
173
+ "2. Reference the KR ID you moved, describe the activity completed, and include notes for blockers or dependencies.",
174
+ "3. Update the quarterly OKR page when `Current` values change materially so the team report reflects the latest status.",
175
+ "",
176
+ "## Set up your personal Chapterhouse instance",
177
+ "1. Install and authenticate Chapterhouse locally.",
178
+ "2. Configure your personal instance to sync team wiki paths for `pages/team`, `pages/okrs`, `pages/kpis`, and `pages/shared`.",
179
+ "3. Confirm your personal Chapterhouse instance can read the seeded team pages before you begin logging updates.",
180
+ "",
181
+ ].join("\n");
182
+ }
183
+ function generateSharedReadme() {
184
+ return [
185
+ "# Shared Team Knowledge",
186
+ "",
187
+ "This space is for team notes, decisions, runbooks, and any knowledge the whole team should have.",
188
+ "Anyone on the team can contribute. Changes are synced to all personal Chapterhouse instances.",
189
+ "",
190
+ "- [OKRs](../okrs/2026-Q2.md)",
191
+ "- [Team KPIs](../kpis/team.md)",
192
+ "",
193
+ ].join("\n");
194
+ }
195
+ function writePageIfMissing(definition) {
196
+ if (readPage(definition.path) === undefined) {
197
+ writePage(definition.path, definition.content);
198
+ return true;
199
+ }
200
+ return false;
201
+ }
202
+ function upsertIndexEntry(definition) {
203
+ const entry = buildIndexEntryForPage(definition.path, definition.fallbackEntry);
204
+ if (entry) {
205
+ addToIndex(entry);
206
+ }
207
+ }
208
+ export function seedTeamWiki() {
209
+ ensureWikiStructure();
210
+ const pages = [
211
+ {
212
+ path: "pages/okrs/2026-Q2.md",
213
+ content: generateOKRQuarterPage("2026-Q2", SAMPLE_OBJECTIVES),
214
+ fallbackEntry: {
215
+ path: "pages/okrs/2026-Q2.md",
216
+ title: "OKRs — 2026 Q2",
217
+ summary: "Q2 2026 team objectives and key results for the Azure platform team.",
218
+ section: "Team",
219
+ tags: ["team", "okrs", "2026-q2"],
220
+ },
221
+ },
222
+ {
223
+ path: "pages/kpis/team.md",
224
+ content: generateKPIPage(SAMPLE_KPIS),
225
+ fallbackEntry: {
226
+ path: "pages/kpis/team.md",
227
+ title: "Team KPIs",
228
+ summary: "Shared delivery, reliability, and cost metrics for the team.",
229
+ section: "Team",
230
+ tags: ["team", "kpis"],
231
+ },
232
+ },
233
+ {
234
+ path: "pages/team/index.md",
235
+ content: generateTeamIndexPage(PLACEHOLDER_MEMBERS),
236
+ fallbackEntry: {
237
+ path: "pages/team/index.md",
238
+ title: "Team Directory",
239
+ summary: "Placeholder team roster with role, identity, and KR ownership fields.",
240
+ section: "Team",
241
+ tags: ["team", "directory"],
242
+ },
243
+ },
244
+ {
245
+ path: "pages/team/onboarding.md",
246
+ content: generateOnboardingPage(),
247
+ fallbackEntry: {
248
+ path: "pages/team/onboarding.md",
249
+ title: "Team Onboarding",
250
+ summary: "How new team members use Chapterhouse, track OKRs, and sync their personal instance.",
251
+ section: "Team",
252
+ tags: ["team", "onboarding"],
253
+ },
254
+ },
255
+ {
256
+ path: "pages/shared/README.md",
257
+ content: generateSharedReadme(),
258
+ fallbackEntry: {
259
+ path: "pages/shared/README.md",
260
+ title: "Shared Team Knowledge",
261
+ summary: "Shared team notes, decisions, and runbooks that sync to every personal Chapterhouse instance.",
262
+ section: "Team",
263
+ tags: ["team", "shared", "runbooks"],
264
+ },
265
+ },
266
+ ];
267
+ const created = [];
268
+ const existing = [];
269
+ for (const page of pages) {
270
+ const wasCreated = writePageIfMissing(page);
271
+ if (wasCreated) {
272
+ created.push(page.path);
273
+ }
274
+ else {
275
+ existing.push(page.path);
276
+ }
277
+ upsertIndexEntry(page);
278
+ }
279
+ return { created, existing };
280
+ }
281
+ async function main() {
282
+ const result = seedTeamWiki();
283
+ const message = result.created.length > 0
284
+ ? `Seeded ${result.created.length} team wiki page(s): ${result.created.join(", ")}`
285
+ : "Team wiki seed already up to date; no pages created.";
286
+ console.log(`[chapterhouse] ${message}`);
287
+ }
288
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
289
+ const modulePath = fileURLToPath(import.meta.url);
290
+ if (invokedPath === modulePath) {
291
+ main().catch((err) => {
292
+ console.error("[chapterhouse] Failed to seed team wiki:", err);
293
+ process.exit(1);
294
+ });
295
+ }
296
+ //# sourceMappingURL=seed-team-wiki.js.map
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { execFile } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import test from "node:test";
9
+ const execFileAsync = promisify(execFile);
10
+ const seedScriptPath = fileURLToPath(new URL("./seed-team-wiki.js", import.meta.url));
11
+ function wikiPath(root, ...parts) {
12
+ return join(root, ".chapterhouse", "wiki", ...parts);
13
+ }
14
+ test("seed-team-wiki script creates starter OKR, KPI, team, shared, and onboarding pages", async () => {
15
+ const chapterhouseHomeRoot = mkdtempSync(join(tmpdir(), "chapterhouse-seed-team-wiki-"));
16
+ try {
17
+ await execFileAsync(process.execPath, [seedScriptPath], {
18
+ cwd: process.cwd(),
19
+ env: {
20
+ ...process.env,
21
+ CHAPTERHOUSE_HOME: chapterhouseHomeRoot,
22
+ },
23
+ });
24
+ const okrPage = readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "okrs", "2026-Q2.md"), "utf-8");
25
+ const kpiPage = readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "kpis", "team.md"), "utf-8");
26
+ const sharedReadme = readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "shared", "README.md"), "utf-8");
27
+ const teamIndex = readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "team", "index.md"), "utf-8");
28
+ const onboarding = readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "team", "onboarding.md"), "utf-8");
29
+ assert.match(okrPage, /^# OKRs — 2026 Q2/m);
30
+ assert.match(okrPage, /## O1:/);
31
+ assert.match(okrPage, /### O2-KR3:/);
32
+ assert.match(kpiPage, /^# Team KPIs/m);
33
+ assert.match(kpiPage, /\| KPI \| Owner \| Target \| Current \| Unit \| Frequency \|/);
34
+ assert.match(sharedReadme, /^# Shared Team Knowledge/m);
35
+ assert.match(sharedReadme, /Anyone on the team can contribute\./i);
36
+ assert.match(sharedReadme, /\.\.\/okrs\/2026-Q2\.md/);
37
+ assert.match(teamIndex, /^# Team Directory/m);
38
+ assert.match(teamIndex, /TBD Engineer 5/);
39
+ assert.match(onboarding, /^# Team Onboarding/m);
40
+ assert.match(onboarding, /set up your personal Chapterhouse instance/i);
41
+ assert.match(onboarding, /log OKR updates/i);
42
+ }
43
+ finally {
44
+ rmSync(chapterhouseHomeRoot, { recursive: true, force: true });
45
+ }
46
+ });
47
+ test("seed-team-wiki script is idempotent and preserves existing pages", async () => {
48
+ const chapterhouseHomeRoot = mkdtempSync(join(tmpdir(), "chapterhouse-seed-team-wiki-idempotent-"));
49
+ const existingOnboarding = "# Team Onboarding\n\nKeep this custom onboarding guide.\n";
50
+ try {
51
+ mkdirSync(wikiPath(chapterhouseHomeRoot, "pages", "team"), { recursive: true });
52
+ writeFileSync(wikiPath(chapterhouseHomeRoot, "pages", "team", "onboarding.md"), existingOnboarding, "utf-8");
53
+ await execFileAsync(process.execPath, [seedScriptPath], {
54
+ cwd: process.cwd(),
55
+ env: {
56
+ ...process.env,
57
+ CHAPTERHOUSE_HOME: chapterhouseHomeRoot,
58
+ },
59
+ });
60
+ assert.equal(readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "team", "onboarding.md"), "utf-8"), existingOnboarding);
61
+ assert.match(readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "okrs", "2026-Q2.md"), "utf-8"), /^# OKRs — 2026 Q2/m);
62
+ assert.match(readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "kpis", "team.md"), "utf-8"), /^# Team KPIs/m);
63
+ assert.match(readFileSync(wikiPath(chapterhouseHomeRoot, "pages", "shared", "README.md"), "utf-8"), /^# Shared Team Knowledge/m);
64
+ }
65
+ finally {
66
+ rmSync(chapterhouseHomeRoot, { recursive: true, force: true });
67
+ }
68
+ });
69
+ //# sourceMappingURL=seed-team-wiki.test.js.map
@@ -0,0 +1,212 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { config } from "../config.js";
4
+ import { WIKI_DIR } from "../paths.js";
5
+ import { assertPagePath, readPage, writePage, writeFileAtomic } from "./fs.js";
6
+ import { addToIndex, buildIndexEntryForPage } from "./index-manager.js";
7
+ export class TeamWikiSync {
8
+ teamChapterhouseUrl;
9
+ teamChapterhouseToken;
10
+ standaloneMode;
11
+ cacheTtlMinutes;
12
+ teamWikiPaths;
13
+ wikiDir;
14
+ cacheRoot;
15
+ manifestPath;
16
+ fetchImpl;
17
+ warn;
18
+ now;
19
+ constructor(options = {}) {
20
+ this.teamChapterhouseUrl = (options.teamChapterhouseUrl ?? config.teamChapterhouseUrl).trim().replace(/\/+$/, "");
21
+ this.teamChapterhouseToken = (options.teamChapterhouseToken ?? config.teamChapterhouseToken).trim();
22
+ this.standaloneMode = options.standaloneMode ?? config.standaloneMode;
23
+ this.cacheTtlMinutes = options.cacheTtlMinutes ?? config.teamWikiCacheTtlMinutes;
24
+ this.teamWikiPaths = (options.teamWikiPaths ?? config.teamWikiPaths)
25
+ .map((path) => path.trim().replace(/\/+$/, ""))
26
+ .filter(Boolean);
27
+ this.wikiDir = options.wikiDir ?? WIKI_DIR;
28
+ this.cacheRoot = join(this.wikiDir, ".team-cache");
29
+ this.manifestPath = join(this.cacheRoot, "manifest.json");
30
+ this.fetchImpl = options.fetchImpl ?? fetch;
31
+ this.warn = options.warn ?? ((message) => console.warn(message));
32
+ this.now = options.now ?? (() => new Date());
33
+ }
34
+ isEnabled() {
35
+ return !this.standaloneMode && this.teamChapterhouseUrl.length > 0;
36
+ }
37
+ isTeamPath(path) {
38
+ return this.teamWikiPaths.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
39
+ }
40
+ async fetchPage(path, options = {}) {
41
+ assertPagePath(path);
42
+ if (!this.isEnabled() || !this.isTeamPath(path)) {
43
+ return null;
44
+ }
45
+ const manifest = this.readManifest();
46
+ const cachedContent = this.readCachedContent(path);
47
+ const manifestEntry = manifest[path];
48
+ if (cachedContent !== undefined && manifestEntry && this.isFresh(manifestEntry)) {
49
+ return cachedContent;
50
+ }
51
+ try {
52
+ const response = await this.fetchImpl(this.buildPageUrl(path), {
53
+ headers: this.buildHeaders(options.authorizationHeader),
54
+ });
55
+ if (!response.ok) {
56
+ throw new Error(`HTTP ${response.status}`);
57
+ }
58
+ const payload = await response.json();
59
+ if (!payload.exists) {
60
+ this.deleteCache(path, manifest);
61
+ return null;
62
+ }
63
+ this.writeCachedContent(path, payload.content);
64
+ manifest[path] = {
65
+ fetchedAt: this.now().toISOString(),
66
+ etag: response.headers.get("etag") ?? undefined,
67
+ };
68
+ this.writeManifest(manifest);
69
+ return payload.content;
70
+ }
71
+ catch (err) {
72
+ if (cachedContent !== undefined) {
73
+ this.warn(`[wiki] Failed to refresh team wiki page "${path}"; serving stale cache. ${err instanceof Error ? err.message : String(err)}`);
74
+ return cachedContent;
75
+ }
76
+ return null;
77
+ }
78
+ }
79
+ async syncAll(options = {}) {
80
+ if (!this.isEnabled()) {
81
+ return [];
82
+ }
83
+ const response = await this.fetchImpl(this.buildListUrl(), {
84
+ headers: this.buildHeaders(options.authorizationHeader),
85
+ });
86
+ if (!response.ok) {
87
+ throw new Error(`Failed to list team wiki pages: HTTP ${response.status}`);
88
+ }
89
+ const payload = await response.json();
90
+ const synced = [];
91
+ for (const path of payload.pages) {
92
+ if (!this.isTeamPath(path)) {
93
+ continue;
94
+ }
95
+ const content = await this.fetchPage(path, options);
96
+ if (content !== null) {
97
+ writePage(path, content);
98
+ const entry = buildIndexEntryForPage(path, { section: "Team" });
99
+ if (entry) {
100
+ addToIndex(entry);
101
+ }
102
+ synced.push(path);
103
+ }
104
+ }
105
+ return synced;
106
+ }
107
+ async pushUpdate(payload, options = {}) {
108
+ if (!this.isEnabled()) {
109
+ return {
110
+ ok: false,
111
+ disabled: true,
112
+ payload,
113
+ };
114
+ }
115
+ const response = await this.fetchImpl(`${this.teamChapterhouseUrl}/api/team/update`, {
116
+ method: "POST",
117
+ headers: {
118
+ ...this.buildHeaders(options.authorizationHeader),
119
+ "content-type": "application/json",
120
+ },
121
+ body: JSON.stringify(payload),
122
+ });
123
+ if (!response.ok) {
124
+ throw new Error(`Failed to push team update: HTTP ${response.status}`);
125
+ }
126
+ return await response.json();
127
+ }
128
+ buildPageUrl(path) {
129
+ return `${this.teamChapterhouseUrl}/api/team/wiki/${encodeURIComponent(path)}`;
130
+ }
131
+ buildListUrl() {
132
+ return `${this.teamChapterhouseUrl}/api/team/wiki`;
133
+ }
134
+ buildHeaders(authorizationHeader) {
135
+ const headers = {
136
+ accept: "application/json",
137
+ };
138
+ const header = authorizationHeader?.trim() || this.normalizeTokenHeader(this.teamChapterhouseToken);
139
+ if (header) {
140
+ headers.authorization = header;
141
+ }
142
+ return headers;
143
+ }
144
+ normalizeTokenHeader(token) {
145
+ if (!token) {
146
+ return undefined;
147
+ }
148
+ return token.startsWith("Bearer ") ? token : `Bearer ${token}`;
149
+ }
150
+ isFresh(entry) {
151
+ const fetchedAtMs = Date.parse(entry.fetchedAt);
152
+ if (!Number.isFinite(fetchedAtMs)) {
153
+ return false;
154
+ }
155
+ return this.now().getTime() - fetchedAtMs < this.cacheTtlMinutes * 60 * 1000;
156
+ }
157
+ readManifest() {
158
+ if (!existsSync(this.manifestPath)) {
159
+ return {};
160
+ }
161
+ try {
162
+ const parsed = JSON.parse(readFileSync(this.manifestPath, "utf-8"));
163
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
164
+ }
165
+ catch {
166
+ return {};
167
+ }
168
+ }
169
+ writeManifest(manifest) {
170
+ mkdirSync(this.cacheRoot, { recursive: true });
171
+ writeFileAtomic(this.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
172
+ }
173
+ readCachedContent(path) {
174
+ const cachePath = this.cacheFilePath(path);
175
+ if (!existsSync(cachePath)) {
176
+ return undefined;
177
+ }
178
+ return readFileSync(cachePath, "utf-8");
179
+ }
180
+ writeCachedContent(path, content) {
181
+ const cachePath = this.cacheFilePath(path);
182
+ mkdirSync(dirname(cachePath), { recursive: true });
183
+ writeFileAtomic(cachePath, content);
184
+ }
185
+ deleteCache(path, manifest) {
186
+ const cachePath = this.cacheFilePath(path);
187
+ if (existsSync(cachePath)) {
188
+ rmSync(cachePath, { force: true });
189
+ }
190
+ if (manifest[path]) {
191
+ delete manifest[path];
192
+ this.writeManifest(manifest);
193
+ }
194
+ }
195
+ cacheFilePath(path) {
196
+ return join(this.cacheRoot, path);
197
+ }
198
+ }
199
+ export const teamWikiSync = new TeamWikiSync();
200
+ export async function readWikiPage(path, options = {}) {
201
+ const shouldUseTeamSync = teamWikiSync.isEnabled()
202
+ && path.startsWith("pages/")
203
+ && teamWikiSync.isTeamPath(path);
204
+ if (shouldUseTeamSync) {
205
+ const remoteContent = await teamWikiSync.fetchPage(path, options);
206
+ if (remoteContent !== null) {
207
+ return remoteContent;
208
+ }
209
+ }
210
+ return readPage(path);
211
+ }
212
+ //# sourceMappingURL=team-sync.js.map