projectkeeper 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +283 -2
  4. package/README.zh-CN.md +285 -0
  5. package/THIRD_PARTY_NOTICES.md +53 -0
  6. package/bin/pk.mjs +6 -0
  7. package/bin/postinstall.mjs +5 -0
  8. package/bin/strip-types.mjs +28 -0
  9. package/package.json +68 -4
  10. package/patches/README.md +111 -0
  11. package/patches/pi-ai-0.85.0.ts +122 -0
  12. package/patches/pi-ai-0.87.1.ts +122 -0
  13. package/scripts/apply-pi-streaming-patch.ts +86 -0
  14. package/skills/clerk/coverage/SKILL.md +35 -0
  15. package/skills/clerk/cross-check/SKILL.md +118 -0
  16. package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
  17. package/skills/clerk/lane-follow-up/SKILL.md +94 -0
  18. package/skills/clerk/lane-plan/SKILL.md +107 -0
  19. package/skills/clerk/lane-slot/SKILL.md +113 -0
  20. package/skills/clerk/lane-topic/SKILL.md +116 -0
  21. package/skills/clerk/orientation/SKILL.md +160 -0
  22. package/skills/clerk/reconcile/SKILL.md +77 -0
  23. package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
  24. package/skills/clerk/spot-check/SKILL.md +52 -0
  25. package/skills/clerk/synthesis/SKILL.md +65 -0
  26. package/skills/table-follow-up/SKILL.md +42 -0
  27. package/src/cli.ts +319 -0
  28. package/src/codemap/README.md +13 -0
  29. package/src/codemap/engines.ts +102 -0
  30. package/src/codemap/facts.ts +504 -0
  31. package/src/context/agent-entry.ts +232 -0
  32. package/src/context/ask.ts +97 -0
  33. package/src/context/assemble.ts +1058 -0
  34. package/src/context/code-source.ts +103 -0
  35. package/src/context/k-briefs.ts +114 -0
  36. package/src/context/lineage.ts +100 -0
  37. package/src/context/object-brief.ts +300 -0
  38. package/src/context/owner-words.ts +191 -0
  39. package/src/context/pack-fixture.ts +342 -0
  40. package/src/context/project-rules.ts +160 -0
  41. package/src/intake/intake.ts +392 -0
  42. package/src/intake/material-rules.ts +166 -0
  43. package/src/intake/removal.ts +316 -0
  44. package/src/intake/skipped.ts +72 -0
  45. package/src/keeper/adjustment.ts +565 -0
  46. package/src/keeper/bounds/boundary.ts +509 -0
  47. package/src/keeper/bounds/command.ts +1665 -0
  48. package/src/keeper/bounds/paths.ts +164 -0
  49. package/src/keeper/bounds/reads.ts +718 -0
  50. package/src/keeper/bounds/shell-write-guard.ts +521 -0
  51. package/src/keeper/capabilities.ts +136 -0
  52. package/src/keeper/clerk-steps.ts +151 -0
  53. package/src/keeper/clerk-tools.ts +1424 -0
  54. package/src/keeper/conversation.ts +253 -0
  55. package/src/keeper/evidence.ts +662 -0
  56. package/src/keeper/fake-provider.ts +132 -0
  57. package/src/keeper/held.ts +15 -0
  58. package/src/keeper/history-tools.ts +163 -0
  59. package/src/keeper/jobs/answering.ts +53 -0
  60. package/src/keeper/key-check.ts +105 -0
  61. package/src/keeper/ledger-tools.ts +295 -0
  62. package/src/keeper/link-check.ts +185 -0
  63. package/src/keeper/merge.ts +260 -0
  64. package/src/keeper/note-origin.ts +56 -0
  65. package/src/keeper/numbers-check.ts +102 -0
  66. package/src/keeper/open-in-pi.ts +57 -0
  67. package/src/keeper/organize/absence.ts +266 -0
  68. package/src/keeper/organize/carried-on.ts +214 -0
  69. package/src/keeper/organize/clerk-coverage.ts +293 -0
  70. package/src/keeper/organize/clerk-prompts.ts +132 -0
  71. package/src/keeper/organize/clerk.ts +1395 -0
  72. package/src/keeper/organize/coverage-gate.ts +19 -0
  73. package/src/keeper/organize/coverage-tools.ts +765 -0
  74. package/src/keeper/organize/entries.ts +153 -0
  75. package/src/keeper/organize/entry-gate.ts +289 -0
  76. package/src/keeper/organize/fill-tools.ts +718 -0
  77. package/src/keeper/organize/follow-up.ts +280 -0
  78. package/src/keeper/organize/generation-check.ts +504 -0
  79. package/src/keeper/organize/graph.ts +230 -0
  80. package/src/keeper/organize/lane-tools.ts +289 -0
  81. package/src/keeper/organize/looked-tools.ts +54 -0
  82. package/src/keeper/organize/materials.ts +243 -0
  83. package/src/keeper/organize/owner-lines.ts +493 -0
  84. package/src/keeper/organize/place-tools.ts +216 -0
  85. package/src/keeper/organize/placement-inference.ts +1045 -0
  86. package/src/keeper/organize/placement-time.ts +320 -0
  87. package/src/keeper/organize/placing.ts +821 -0
  88. package/src/keeper/organize/positioning.ts +33 -0
  89. package/src/keeper/organize/readiness.ts +47 -0
  90. package/src/keeper/organize/reading.ts +401 -0
  91. package/src/keeper/organize/round-blocks.ts +65 -0
  92. package/src/keeper/organize/round-news.ts +164 -0
  93. package/src/keeper/organize/round-open.ts +586 -0
  94. package/src/keeper/organize/schedule-state.ts +39 -0
  95. package/src/keeper/organize/schedule.ts +137 -0
  96. package/src/keeper/organize/service.ts +409 -0
  97. package/src/keeper/organize/skills.ts +116 -0
  98. package/src/keeper/organize/slots.ts +85 -0
  99. package/src/keeper/organize/stage-gate.ts +175 -0
  100. package/src/keeper/organize/stage-tools.ts +471 -0
  101. package/src/keeper/organize/standing-notes.ts +141 -0
  102. package/src/keeper/organize/table-follow-up.ts +330 -0
  103. package/src/keeper/organize/takeover-clerk.ts +832 -0
  104. package/src/keeper/organize/takeover-state.ts +103 -0
  105. package/src/keeper/organize/takeover.ts +216 -0
  106. package/src/keeper/organize/update-pending.ts +535 -0
  107. package/src/keeper/organize/workbench-placement.ts +92 -0
  108. package/src/keeper/own-keys.ts +114 -0
  109. package/src/keeper/owner-text.ts +165 -0
  110. package/src/keeper/project-folder.ts +334 -0
  111. package/src/keeper/prompts.ts +101 -0
  112. package/src/keeper/references.ts +177 -0
  113. package/src/keeper/relook/compose.ts +120 -0
  114. package/src/keeper/relook/prompts.ts +36 -0
  115. package/src/keeper/roles.ts +259 -0
  116. package/src/keeper/round-tools.ts +227 -0
  117. package/src/keeper/route.ts +148 -0
  118. package/src/keeper/rules.ts +97 -0
  119. package/src/keeper/runtime.ts +1858 -0
  120. package/src/keeper/scope-tools.ts +194 -0
  121. package/src/keeper/sendback-text.ts +83 -0
  122. package/src/keeper/step-timing.ts +207 -0
  123. package/src/keeper/testing/clerk-script.ts +212 -0
  124. package/src/keeper/testing/streaming-arguments.ts +57 -0
  125. package/src/keeper/tools.ts +2340 -0
  126. package/src/keeper/utterance-tools.ts +64 -0
  127. package/src/keeper/verbatim.ts +397 -0
  128. package/src/ledger/adapters.ts +186 -0
  129. package/src/ledger/arrangements.ts +257 -0
  130. package/src/ledger/blame.ts +58 -0
  131. package/src/ledger/code-engine-child.ts +69 -0
  132. package/src/ledger/code-engine.ts +548 -0
  133. package/src/ledger/code.ts +607 -0
  134. package/src/ledger/docs.ts +294 -0
  135. package/src/ledger/git-read.ts +321 -0
  136. package/src/ledger/index.ts +1850 -0
  137. package/src/ledger/item-range.ts +16 -0
  138. package/src/ledger/lines.ts +328 -0
  139. package/src/ledger/numbering.ts +269 -0
  140. package/src/ledger/ranges.ts +74 -0
  141. package/src/ledger/rebuild-worker.ts +15 -0
  142. package/src/ledger/rebuild.ts +387 -0
  143. package/src/ledger/repo-scan.ts +181 -0
  144. package/src/ledger/schema.ts +450 -0
  145. package/src/ledger/sessions.ts +134 -0
  146. package/src/ledger/text-scan.ts +466 -0
  147. package/src/ledger/time.ts +185 -0
  148. package/src/ledger/ts-symbols.ts +180 -0
  149. package/src/ledger/views.ts +377 -0
  150. package/src/mcp/server.ts +127 -0
  151. package/src/mcp/tools.ts +187 -0
  152. package/src/model/ids.ts +23 -0
  153. package/src/model/k-types.ts +912 -0
  154. package/src/model/time.ts +85 -0
  155. package/src/model/types.ts +1382 -0
  156. package/src/model/views-k.ts +707 -0
  157. package/src/model/vocab.ts +269 -0
  158. package/src/process/analysis.ts +79 -0
  159. package/src/process/breakpoint-candidates.ts +383 -0
  160. package/src/process/breakpoints.ts +746 -0
  161. package/src/process/claim-check.ts +295 -0
  162. package/src/process/delivery-links.ts +52 -0
  163. package/src/process/facts.ts +487 -0
  164. package/src/process/generations.ts +138 -0
  165. package/src/process/index.ts +243 -0
  166. package/src/process/integration.ts +128 -0
  167. package/src/process/placement.ts +107 -0
  168. package/src/process/plan.ts +276 -0
  169. package/src/process/sendbacks.ts +244 -0
  170. package/src/process/text.ts +185 -0
  171. package/src/process/units.ts +543 -0
  172. package/src/process/unnumbered.ts +431 -0
  173. package/src/process/work.ts +884 -0
  174. package/src/scope/candidates.ts +86 -0
  175. package/src/scope/decisions.ts +229 -0
  176. package/src/scope/discover.ts +741 -0
  177. package/src/scope/ignore.ts +126 -0
  178. package/src/scope/ignored-place.ts +93 -0
  179. package/src/scope/skip.ts +161 -0
  180. package/src/scope/toolchain.ts +180 -0
  181. package/src/scope/worktree.ts +187 -0
  182. package/src/server/api.ts +549 -0
  183. package/src/server/app.ts +595 -0
  184. package/src/server/drafts-api.ts +170 -0
  185. package/src/server/folders-api.ts +243 -0
  186. package/src/server/graph-view.ts +639 -0
  187. package/src/server/http.ts +224 -0
  188. package/src/server/k-agent-api.ts +56 -0
  189. package/src/server/k-rounds-d99.ts +91 -0
  190. package/src/server/k-views.ts +405 -0
  191. package/src/server/keeper-page.ts +446 -0
  192. package/src/server/keys-api.ts +239 -0
  193. package/src/server/project-folder-api.ts +104 -0
  194. package/src/server/vendor.ts +14 -0
  195. package/src/server/workbench-content.ts +561 -0
  196. package/src/sources/anchor.ts +129 -0
  197. package/src/sources/files.ts +267 -0
  198. package/src/sources/gitobs.ts +41 -0
  199. package/src/sources/history.ts +231 -0
  200. package/src/sources/scan-worker.ts +10 -0
  201. package/src/sources/sessions/locate.ts +292 -0
  202. package/src/sources/sessions/read.ts +346 -0
  203. package/src/sources/sessions/scope.ts +42 -0
  204. package/src/sources/sessions/utterances.ts +223 -0
  205. package/src/sources/watch.ts +215 -0
  206. package/src/store/json-file.ts +50 -0
  207. package/src/store/paths.ts +39 -0
  208. package/src/store/project-store.ts +295 -0
  209. package/src/store/versions.ts +172 -0
  210. package/src/store/workspace.ts +216 -0
  211. package/src/table/check.ts +333 -0
  212. package/src/table/follow-up-tools.ts +30 -0
  213. package/src/table/follow-up.ts +344 -0
  214. package/src/table/import.ts +610 -0
  215. package/src/table/project-home.ts +61 -0
  216. package/src/table/sessions.ts +168 -0
  217. package/src/table/takeover.ts +483 -0
  218. package/src/util/git-check.ts +29 -0
  219. package/src/util/git.ts +411 -0
  220. package/src/util/own-ts.ts +15 -0
  221. package/src/util/paths.ts +225 -0
  222. package/template/FILLING-IN.md +148 -0
  223. package/template/examples/papertrail.json +747 -0
  224. package/template/projectkeeper.schema.json +1001 -0
  225. package/ui/app.js +1135 -0
  226. package/ui/failures.js +82 -0
  227. package/ui/flyout.js +120 -0
  228. package/ui/folder-chooser.js +264 -0
  229. package/ui/glossary.js +156 -0
  230. package/ui/graph-fit.js +157 -0
  231. package/ui/graph-material.js +61 -0
  232. package/ui/graph-palette.js +109 -0
  233. package/ui/graph-tools.js +62 -0
  234. package/ui/graph.js +1712 -0
  235. package/ui/index.html +31 -0
  236. package/ui/k/code-view.js +230 -0
  237. package/ui/k/draft-view.js +93 -0
  238. package/ui/k/k-views.css +224 -0
  239. package/ui/k/round-tree.js +450 -0
  240. package/ui/k/scope-k.js +159 -0
  241. package/ui/k-data.js +118 -0
  242. package/ui/k-fold.js +106 -0
  243. package/ui/k-process.css +302 -0
  244. package/ui/k-process.js +934 -0
  245. package/ui/keeper-activity.js +175 -0
  246. package/ui/keeper-view.js +801 -0
  247. package/ui/keeper.css +101 -0
  248. package/ui/key-order.js +62 -0
  249. package/ui/list-fold.js +67 -0
  250. package/ui/markdown.d.ts +21 -0
  251. package/ui/markdown.js +472 -0
  252. package/ui/placement.d.ts +92 -0
  253. package/ui/placement.js +445 -0
  254. package/ui/popover-place.d.ts +16 -0
  255. package/ui/popover-place.js +89 -0
  256. package/ui/popover.js +198 -0
  257. package/ui/project-folder.js +37 -0
  258. package/ui/shell-state.js +40 -0
  259. package/ui/strip-rows.js +171 -0
  260. package/ui/styles.css +492 -0
  261. package/ui/theme-pick.js +36 -0
  262. package/ui/theme.js +74 -0
  263. package/ui/themes/LICENSE.md +13 -0
  264. package/ui/themes/README.md +44 -0
  265. package/ui/themes/TOKENS.md +291 -0
  266. package/ui/themes/a1/board.css +1557 -0
  267. package/ui/themes/a1/pk.css +392 -0
  268. package/ui/themes/b1/board.css +1163 -0
  269. package/ui/themes/b1/pk.css +218 -0
  270. package/ui/themes/d2/board.css +518 -0
  271. package/ui/themes/d2/pk.css +243 -0
  272. package/ui/themes/guard.css +73 -0
  273. package/ui/themes/i1/board.css +776 -0
  274. package/ui/themes/i1/pk.css +223 -0
  275. package/ui/themes/manifest.json +13 -0
  276. package/ui/views.js +2796 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A project opened for table work (`pk import`, `pk sessions`): the home opened with no organizing, the project found by
3
+ * its folder or added and read in, and its ledger brought up to date — nothing the Keeper would start on its own runs.
4
+ * One implementation for every table command, so taking the table in and exporting the session material for it see the
5
+ * project the same way (D107).
6
+ */
7
+ import { existsSync } from 'node:fs';
8
+ import type { App } from '../server/app.ts';
9
+ import type { Project } from '../model/types.ts';
10
+ import { projectKeeperHome, workspaceFile } from '../store/paths.ts';
11
+ import { canonicalPath, sameDirectory } from '../util/paths.ts';
12
+
13
+ export interface TableProjectOptions {
14
+ /** The project folder, as the owner gave it. */
15
+ readonly projectDir: string;
16
+ /** The ProjectKeeper home; by default the user's (store/paths.ts). */
17
+ readonly home?: string;
18
+ /** The project's name when it is added. */
19
+ readonly name: string;
20
+ }
21
+
22
+ export interface TableProject {
23
+ readonly app: App;
24
+ /** The project as it stands after intake and the ledger rebuild. */
25
+ readonly project: Project;
26
+ readonly home: string;
27
+ /** The project was not in the home and was added. */
28
+ readonly added: boolean;
29
+ /** What did not go as it should and did not stop the work (the ledger left as it was). */
30
+ readonly warnings: string[];
31
+ }
32
+
33
+ /**
34
+ * Open the project for table work, hand it to `work`, and close the home whatever happens: watchers stopped, the
35
+ * ledger's connections released, every store written. The App is loaded only when this is called, so a command that
36
+ * refuses its input before calling it opens nothing.
37
+ */
38
+ export async function withTableProject<T>(options: TableProjectOptions, work: (opened: TableProject) => Promise<T>): Promise<T> {
39
+ const { App } = await import('../server/app.ts');
40
+ const projectDir = canonicalPath(options.projectDir);
41
+ const home = canonicalPath(options.home ?? projectKeeperHome());
42
+ const made = !existsSync(workspaceFile(home));
43
+ const app = new App(home, { organizing: false });
44
+ const warnings: string[] = [];
45
+ try {
46
+ // A home made for the table works through what it holds, as the fixture's does: nothing watches the project.
47
+ if (made) app.workspace.setSettings({ watchProjects: false });
48
+ let project = app.workspace.list().find((x) => x.locations.some((l) => sameDirectory(l, projectDir)));
49
+ const added = !project;
50
+ if (!project) project = app.addProject(options.name, [projectDir]);
51
+ // What intake reads is what the table's sources are matched against, the scope the boundary draws, and the session
52
+ // sources the ledger reads the owner's words from.
53
+ if (added || !project.lastScopedAt) await app.intakeProject(project.id);
54
+ app.stopAll();
55
+ try { app.ledger.rebuildNow(app.project(project.id)); } catch (e) { warnings.push(`The ledger was not brought up to date: ${(e as Error).message}`); }
56
+ return await work({ app, project: app.project(project.id), home, added, warnings });
57
+ } finally {
58
+ app.stopAll();
59
+ await app.flushAll();
60
+ }
61
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * `pk sessions`: the session material a takeover's first step reads the owner's words from (template/FILLING-IN.md).
3
+ * Every owner message of the project's agent sessions, oldest first, each after the agent's last answer it replies to,
4
+ * written twice: `session-material.md` for the agent that fills `ownerWants`, `session-material.jsonl` for `pk check`
5
+ * and `pk import` to hold its citations to (check.ts `readSessionMaterial`). A session is named `host:session id` and a
6
+ * message by the ledger's number for it, which is how the table cites one: { "session": "claude:…", "message": 12 }.
7
+ *
8
+ * Read from the project's ledger, which already keeps the owner's messages and the agent messages they answer, redacted
9
+ * as they were recorded; nothing more is redacted here. Headless runs are left out: their prompt is a program's, not the
10
+ * owner's. The material carries the owner's words and whatever else was said in the sessions, so it is written into
11
+ * ProjectKeeper's own folder for the project and never into the project, which may be a public repository (D107).
12
+ */
13
+ import { mkdirSync, writeFileSync } from 'node:fs';
14
+ import { basename, join, resolve } from 'node:path';
15
+ import type { Ledger, MessageRow } from '../ledger/index.ts';
16
+ import { projectDir as homeProjectDir, tableDir } from '../store/paths.ts';
17
+ import { canonicalPath, isWithin } from '../util/paths.ts';
18
+ import { withTableProject } from './project-home.ts';
19
+
20
+ export const MARKDOWN_FILE = 'session-material.md';
21
+ export const JSONL_FILE = 'session-material.jsonl';
22
+
23
+ /** One record of the JSONL: what `pk check` reads (check.ts `SessionMessage`) and who said it. */
24
+ export interface SessionRecord {
25
+ readonly session: string;
26
+ readonly message: number;
27
+ readonly role: 'agent' | 'owner';
28
+ readonly text: string;
29
+ }
30
+
31
+ export interface SessionMaterialCounts {
32
+ /** Owner messages written. */
33
+ readonly ownerMessages: number;
34
+ /** JSONL records: the owner messages and the agent answers before them. */
35
+ readonly records: number;
36
+ /** Sessions with at least one owner message written. */
37
+ readonly sessions: number;
38
+ }
39
+
40
+ /** An owner message as the ledger's ownerWords query gives it: with its session's host and id, and the answer before it. */
41
+ export type OwnerRow = MessageRow & { readonly host: string; readonly sessionId: string };
42
+
43
+ /** The name the material and the table give a session: `host:session id`. */
44
+ export const sessionName = (r: Pick<OwnerRow, 'host' | 'sessionId'>): string => `${r.host}:${r.sessionId}`;
45
+
46
+ /** `2026-09-20 01:00 UTC` for a recorded time; a message the log gave no time keeps saying so. */
47
+ function when(row: MessageRow): string {
48
+ if (row.occurred.undated) return 'time not recorded';
49
+ const ms = Date.parse(row.occurred.at);
50
+ return Number.isNaN(ms) ? row.occurred.at : `${new Date(ms).toISOString().slice(0, 16).replace('T', ' ')} UTC`;
51
+ }
52
+
53
+ /**
54
+ * Every owner message of the ledger outside headless runs, oldest first (by time, then session, then number); with
55
+ * `since`, those from then on (the table follow-up's new messages since the table was filled in, D107).
56
+ */
57
+ export function ownerMessages(ledger: Ledger, since?: string | null): OwnerRow[] {
58
+ const rows: OwnerRow[] = [];
59
+ for (let offset: number | null = 0; offset !== null;) {
60
+ const page = ledger.ownerWords({ offset, limit: 500, ...(since ? { since } : {}) });
61
+ if (typeof page === 'string') throw new Error(page); // only a named session can fail, and none is named
62
+ rows.push(...(page.rows as OwnerRow[]));
63
+ offset = page.next;
64
+ }
65
+ const key = (r: OwnerRow) => (r.occurred.undated ? Number.POSITIVE_INFINITY : Date.parse(r.occurred.at));
66
+ const name = sessionName;
67
+ return rows.sort((a, b) => key(a) - key(b) || (name(a) < name(b) ? -1 : name(a) > name(b) ? 1 : 0) || a.index - b.index);
68
+ }
69
+
70
+ /** The JSONL records of owner messages: each agent answer before an owner message, then the message; each once. */
71
+ export function materialRecords(owners: readonly OwnerRow[]): SessionRecord[] {
72
+ const records: SessionRecord[] = [];
73
+ const written = new Set<string>();
74
+ const record = (r: SessionRecord) => {
75
+ const key = `${r.session}#${r.message}`;
76
+ if (written.has(key)) return;
77
+ written.add(key);
78
+ records.push(r);
79
+ };
80
+ for (const m of owners) {
81
+ const session = sessionName(m);
82
+ if (m.answers) record({ session, message: m.answers.index, role: 'agent', text: m.answers.text ?? '' });
83
+ record({ session, message: m.index, role: 'owner', text: m.text ?? '' });
84
+ }
85
+ return records;
86
+ }
87
+
88
+ /**
89
+ * Write the session material of a project's ledger into `outDir`: the Markdown an agent reads and the JSONL the checks
90
+ * read. Returns what was written.
91
+ */
92
+ export function exportSessionMaterial(ledger: Ledger, outDir: string): SessionMaterialCounts & { readonly markdown: string; readonly jsonl: string } {
93
+ const owners = ownerMessages(ledger);
94
+ const md: string[] = [
95
+ '# Session material',
96
+ '',
97
+ 'The owner\'s messages in the project\'s agent sessions, oldest first. Each owner message comes after the agent\'s last answer it replies to (its text only: the tool calls are left out). '
98
+ + 'Cite a message as { "session": "<name>", "message": <number> }: the name is the first part of its heading and the number is the message\'s own.',
99
+ ];
100
+ const records = materialRecords(owners);
101
+ const sessions = new Set<string>();
102
+ for (const m of owners) {
103
+ const session = sessionName(m);
104
+ sessions.add(session);
105
+ md.push('', `## ${session} · message ${m.index} · ${when(m)}`, '');
106
+ const a = m.answers;
107
+ if (a) md.push(`Agent, message ${a.index}:`, '', '<agent>', a.text ?? '', '</agent>', '');
108
+ else md.push('(No agent answer before it: the session starts here.)', '');
109
+ md.push(`Owner, message ${m.index}:`, '', '<owner>', m.text ?? '', '</owner>');
110
+ }
111
+ mkdirSync(outDir, { recursive: true });
112
+ const markdown = join(outDir, MARKDOWN_FILE);
113
+ const jsonl = join(outDir, JSONL_FILE);
114
+ writeFileSync(markdown, `${md.join('\n')}\n`);
115
+ writeFileSync(jsonl, records.map((r) => JSON.stringify(r)).join('\n') + (records.length ? '\n' : ''));
116
+ return { ownerMessages: owners.length, records: records.length, sessions: sessions.size, markdown, jsonl };
117
+ }
118
+
119
+ export interface SessionsOptions {
120
+ /** The project folder. */
121
+ readonly projectDir: string;
122
+ /** The ProjectKeeper home; by default the user's. */
123
+ readonly home?: string;
124
+ /** The project's name when it is added; by default the folder's. */
125
+ readonly name?: string;
126
+ /** Where to write; by default `<home>/projects/<project id>/table`. Never inside the project (D107). */
127
+ readonly out?: string;
128
+ }
129
+
130
+ export interface SessionsResult extends SessionMaterialCounts {
131
+ readonly projectId: string;
132
+ readonly projectName: string;
133
+ readonly home: string;
134
+ readonly added: boolean;
135
+ readonly outDir: string;
136
+ readonly markdown: string;
137
+ readonly jsonl: string;
138
+ readonly warnings: readonly string[];
139
+ }
140
+
141
+ /** Why an output folder inside the project is refused, in a sentence; null when it is outside every location. */
142
+ export function refusedOut(out: string, locations: readonly string[]): string | null {
143
+ const dir = canonicalPath(out);
144
+ const inside = locations.find((l) => isWithin(canonicalPath(l), dir));
145
+ return inside === undefined ? null
146
+ : `${dir} is inside the project folder ${inside}. The session material carries the owner's words and whatever else was said in the sessions, and the project may be a public repository, so it is written only outside the project: leave out --out to write it into ProjectKeeper's own folder for the project (D107).`;
147
+ }
148
+
149
+ /**
150
+ * Open the project (found by its folder or added and read in; its ledger brought up to date; nothing organizing) and
151
+ * write its session material. An output folder inside the project is refused before anything is opened.
152
+ */
153
+ export async function exportProjectSessions(options: SessionsOptions): Promise<SessionsResult> {
154
+ const projectDir = canonicalPath(options.projectDir);
155
+ const out = options.out === undefined ? null : resolve(options.out);
156
+ const refused = out ? refusedOut(out, [projectDir]) : null;
157
+ if (refused) throw new Error(refused);
158
+ return withTableProject({ projectDir, home: options.home, name: options.name ?? basename(projectDir) }, async ({ app, project, home, added, warnings }) => {
159
+ // The project may have more locations than the folder given: the material lands in none of them.
160
+ const elsewhere = out ? refusedOut(out, project.locations) : null;
161
+ if (elsewhere) throw new Error(elsewhere);
162
+ const ledger = app.ledger.ledger(project.id);
163
+ if (!ledger) throw new Error(`The ledger of ${project.name} could not be built, so there are no sessions to export.${warnings.length ? ` ${warnings.join(' ')}` : ''}`);
164
+ const outDir = out ?? tableDir(project.id, home);
165
+ const written = exportSessionMaterial(ledger, outDir);
166
+ return { projectId: project.id, projectName: project.name, home, added, outDir, warnings, ...written };
167
+ });
168
+ }