@prover-coder-ai/context-doc 1.0.12 → 1.0.13

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 (49) hide show
  1. package/.jscpd.json +16 -0
  2. package/CHANGELOG.md +13 -0
  3. package/bin/context-doc.js +12 -0
  4. package/biome.json +37 -0
  5. package/eslint.config.mts +305 -0
  6. package/eslint.effect-ts-check.config.mjs +220 -0
  7. package/linter.config.json +33 -0
  8. package/package.json +72 -41
  9. package/src/app/main.ts +29 -0
  10. package/src/app/program.ts +32 -0
  11. package/src/core/knowledge.ts +93 -117
  12. package/src/shell/cli.ts +73 -5
  13. package/src/shell/services/crypto.ts +50 -0
  14. package/src/shell/services/file-system.ts +142 -0
  15. package/src/shell/services/runtime-env.ts +54 -0
  16. package/src/shell/sync/index.ts +35 -0
  17. package/src/shell/sync/shared.ts +229 -0
  18. package/src/shell/sync/sources/claude.ts +54 -0
  19. package/src/shell/sync/sources/codex.ts +261 -0
  20. package/src/shell/sync/sources/qwen.ts +97 -0
  21. package/src/shell/sync/types.ts +47 -0
  22. package/tests/core/knowledge.test.ts +95 -0
  23. package/tests/shell/cli-pack.test.ts +176 -0
  24. package/tests/shell/sync-knowledge.test.ts +203 -0
  25. package/tests/support/fs-helpers.ts +50 -0
  26. package/tsconfig.json +19 -0
  27. package/vite.config.ts +32 -0
  28. package/vitest.config.ts +70 -66
  29. package/README.md +0 -42
  30. package/dist/core/greeting.js +0 -25
  31. package/dist/core/knowledge.js +0 -49
  32. package/dist/main.js +0 -27
  33. package/dist/shell/claudeSync.js +0 -23
  34. package/dist/shell/cli.js +0 -5
  35. package/dist/shell/codexSync.js +0 -129
  36. package/dist/shell/qwenSync.js +0 -41
  37. package/dist/shell/syncKnowledge.js +0 -39
  38. package/dist/shell/syncShared.js +0 -49
  39. package/dist/shell/syncTypes.js +0 -1
  40. package/src/core/greeting.ts +0 -39
  41. package/src/main.ts +0 -49
  42. package/src/shell/claudeSync.ts +0 -55
  43. package/src/shell/codexSync.ts +0 -236
  44. package/src/shell/qwenSync.ts +0 -73
  45. package/src/shell/syncKnowledge.ts +0 -49
  46. package/src/shell/syncShared.ts +0 -94
  47. package/src/shell/syncTypes.ts +0 -30
  48. package/src/types/env.d.ts +0 -10
  49. package/tsconfig.build.json +0 -18
@@ -1,129 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import * as S from "@effect/schema/Schema";
5
- import { Console, Effect, pipe } from "effect";
6
- import { buildProjectLocator, linesMatchProject } from "../core/knowledge.js";
7
- import { ensureDirectory, syncError } from "./syncShared.js";
8
- const PackageRepositorySchema = S.Struct({
9
- url: S.String,
10
- });
11
- const PackageFileSchema = S.Struct({
12
- repository: S.optional(PackageRepositorySchema),
13
- });
14
- const PackageFileFromJson = S.compose(S.parseJson(), PackageFileSchema);
15
- const decodePackageFile = S.decodeUnknownSync(PackageFileFromJson);
16
- const readRepositoryUrl = (cwd, repositoryUrlOverride) => Effect.try({
17
- try: () => {
18
- if (repositoryUrlOverride !== undefined) {
19
- return repositoryUrlOverride;
20
- }
21
- const raw = fs.readFileSync(path.join(cwd, "package.json"), "utf8");
22
- const parsed = decodePackageFile(raw);
23
- const repositoryUrl = parsed.repository?.url;
24
- if (repositoryUrl === undefined) {
25
- throw new Error("repository url is missing");
26
- }
27
- return repositoryUrl;
28
- },
29
- catch: () => syncError("package.json", "Cannot read repository url"),
30
- });
31
- const resolveSourceDir = (cwd, override, metaRoot) => pipe(Effect.sync(() => {
32
- const envSource = process.env.CODEX_SOURCE_DIR;
33
- const metaCandidate = metaRoot === undefined
34
- ? undefined
35
- : metaRoot.endsWith(".codex")
36
- ? metaRoot
37
- : path.join(metaRoot, ".codex");
38
- const localSource = path.join(cwd, ".codex");
39
- const homeSource = path.join(os.homedir(), ".codex");
40
- const localKnowledge = path.join(cwd, ".knowledge", ".codex");
41
- const homeKnowledge = path.join(os.homedir(), ".knowledge", ".codex");
42
- const candidates = [
43
- override,
44
- envSource,
45
- metaCandidate,
46
- localSource,
47
- homeSource,
48
- localKnowledge,
49
- homeKnowledge,
50
- ].filter((candidate) => candidate !== undefined);
51
- const existing = candidates.find((candidate) => fs.existsSync(candidate));
52
- return { existing, candidates };
53
- }), Effect.flatMap(({ existing, candidates }) => existing === undefined
54
- ? Effect.fail(syncError(".codex", `Source .codex directory is missing; checked: ${candidates.join(", ")}`))
55
- : Effect.succeed(existing)));
56
- const collectJsonlFiles = (root) => Effect.try({
57
- try: () => {
58
- const collected = [];
59
- const walk = (current) => {
60
- const entries = fs.readdirSync(current, { withFileTypes: true });
61
- for (const entry of entries) {
62
- const fullPath = path.join(current, entry.name);
63
- if (entry.isDirectory()) {
64
- walk(fullPath);
65
- }
66
- else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
67
- collected.push(fullPath);
68
- }
69
- }
70
- };
71
- walk(root);
72
- return collected;
73
- },
74
- catch: () => syncError(root, "Cannot traverse .codex"),
75
- });
76
- const isFileRelevant = (filePath, locator) => Effect.try({
77
- try: () => {
78
- const content = fs.readFileSync(filePath, "utf8");
79
- const lines = content.split("\n");
80
- return linesMatchProject(lines, locator);
81
- },
82
- catch: () => syncError(filePath, "Cannot read jsonl file"),
83
- });
84
- const copyRelevantFile = (sourceRoot, destinationRoot, filePath) => Effect.try({
85
- try: () => {
86
- const relative = path.relative(sourceRoot, filePath);
87
- const targetPath = path.join(destinationRoot, relative);
88
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
89
- fs.copyFileSync(filePath, targetPath);
90
- },
91
- catch: () => syncError(filePath, "Cannot copy file into .knowledge/.codex"),
92
- });
93
- const selectRelevantFiles = (files, locator) => Effect.reduce(files, [], (acc, filePath) => pipe(isFileRelevant(filePath, locator), Effect.map((isRelevant) => {
94
- if (isRelevant) {
95
- acc.push(filePath);
96
- }
97
- return acc;
98
- })));
99
- const resolveLocator = (options) => pipe(readRepositoryUrl(options.cwd, options.repositoryUrlOverride), Effect.map((repositoryUrl) => buildProjectLocator(repositoryUrl, options.cwd)), Effect.catchAll(() => Effect.gen(function* (__) {
100
- yield* __(Console.log("Codex repository url missing; falling back to cwd-only match"));
101
- return buildProjectLocator(options.cwd, options.cwd);
102
- })));
103
- const copyCodexFiles = (sourceDir, destinationDir, locator) => Effect.gen(function* (_) {
104
- yield* _(ensureDirectory(destinationDir));
105
- const allJsonlFiles = yield* _(collectJsonlFiles(sourceDir));
106
- const relevantFiles = yield* _(selectRelevantFiles(allJsonlFiles, locator));
107
- yield* _(Effect.forEach(relevantFiles, (filePath) => copyRelevantFile(sourceDir, destinationDir, filePath)));
108
- yield* _(Console.log(`Codex: copied ${relevantFiles.length} files from ${sourceDir} to ${destinationDir}`));
109
- });
110
- // CHANGE: Extract Codex dialog sync into dedicated module for clarity.
111
- // WHY: Separate Codex-specific shell effects from other sync flows.
112
- // QUOTE(ТЗ): "вынеси в отдельный файл"
113
- // REF: user request 2025-11-26
114
- // SOURCE: internal requirement
115
- // FORMAT THEOREM: ∀f ∈ Files: relevant(f, locator) → copied(f, destination)
116
- // PURITY: SHELL
117
- // EFFECT: Effect<void, SyncError, never>
118
- // INVARIANT: ∀f ∈ copiedFiles: linesMatchProject(f, locator)
119
- // COMPLEXITY: O(n) time / O(n) space, n = |files|
120
- export const syncCodex = (options) => Effect.gen(function* (_) {
121
- const locator = yield* _(resolveLocator(options));
122
- const sourceDir = yield* _(resolveSourceDir(options.cwd, options.sourceDir, options.metaRoot));
123
- const destinationDir = options.destinationDir ?? path.join(options.cwd, ".knowledge", ".codex");
124
- if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
125
- yield* _(Console.log("Codex source equals destination; skipping copy to avoid duplicates"));
126
- return;
127
- }
128
- yield* _(copyCodexFiles(sourceDir, destinationDir, locator));
129
- }).pipe(Effect.catchAll((error) => Console.log(`Codex source not found; skipped syncing Codex dialog files (${error.reason})`)));
@@ -1,41 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { Effect } from "effect";
6
- import { copyFilteredFiles, runSyncSource, syncError } from "./syncShared.js";
7
- const qwenHashFromPath = (projectPath) => createHash("sha256").update(projectPath).digest("hex");
8
- const resolveQwenSourceDir = (cwd, override, metaRoot) => Effect.gen(function* (_) {
9
- const hash = qwenHashFromPath(cwd);
10
- const envSource = process.env.QWEN_SOURCE_DIR;
11
- const baseFromMeta = metaRoot === undefined
12
- ? undefined
13
- : metaRoot.endsWith(".qwen")
14
- ? metaRoot
15
- : path.join(metaRoot, ".qwen");
16
- const metaKnowledge = path.join(metaRoot ?? "", ".knowledge", ".qwen");
17
- const homeBase = path.join(os.homedir(), ".qwen");
18
- const homeKnowledge = path.join(os.homedir(), ".knowledge", ".qwen");
19
- const candidates = [
20
- override,
21
- envSource,
22
- baseFromMeta ? path.join(baseFromMeta, "tmp", hash) : undefined,
23
- path.join(cwd, ".qwen", "tmp", hash),
24
- path.join(cwd, ".knowledge", ".qwen", "tmp", hash),
25
- metaKnowledge ? path.join(metaKnowledge, "tmp", hash) : undefined,
26
- path.join(homeBase, "tmp", hash),
27
- path.join(homeKnowledge, "tmp", hash),
28
- ].filter((candidate) => candidate !== undefined);
29
- const found = candidates.find((candidate) => fs.existsSync(candidate));
30
- if (found === undefined) {
31
- return yield* _(Effect.fail(syncError(".qwen", `Qwen source directory is missing for hash ${hash}`)));
32
- }
33
- return found;
34
- });
35
- export const syncQwen = (options) => runSyncSource(qwenSource, options);
36
- const qwenSource = {
37
- name: "Qwen",
38
- destSubdir: ".qwen",
39
- resolveSource: (options) => resolveQwenSourceDir(options.cwd, options.qwenSourceDir, options.metaRoot),
40
- copy: (sourceDir, destinationDir) => copyFilteredFiles(sourceDir, destinationDir, (entry, fullPath) => entry.isFile() && fullPath.endsWith(".json"), "Cannot traverse Qwen directory"),
41
- };
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Effect } from "effect";
3
- import { syncClaude } from "./claudeSync.js";
4
- import { syncCodex } from "./codexSync.js";
5
- import { syncQwen } from "./qwenSync.js";
6
- // PURPOSE: Sync project-scoped dialogs (Codex + Qwen) into .knowledge storage.
7
- export const buildSyncProgram = (options) => Effect.gen(function* (_) {
8
- yield* _(syncClaude(options));
9
- yield* _(syncCodex(options));
10
- yield* _(syncQwen(options));
11
- });
12
- export const parseArgs = () => {
13
- const argv = process.argv.slice(2);
14
- let result = { cwd: process.cwd() };
15
- const mapping = {
16
- "--source": "sourceDir",
17
- "-s": "sourceDir",
18
- "--dest": "destinationDir",
19
- "-d": "destinationDir",
20
- "--project-url": "repositoryUrlOverride",
21
- "--project-name": "repositoryUrlOverride",
22
- "--meta-root": "metaRoot",
23
- "--qwen-source": "qwenSourceDir",
24
- "--claude-projects": "claudeProjectsRoot",
25
- };
26
- for (let i = 0; i < argv.length; i++) {
27
- const arg = argv[i];
28
- const key = mapping[arg];
29
- if (key === undefined) {
30
- continue;
31
- }
32
- const value = argv[i + 1];
33
- if (value !== undefined) {
34
- result = { ...result, [key]: value };
35
- i++;
36
- }
37
- }
38
- return result;
39
- };
@@ -1,49 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { Console, Effect } from "effect";
4
- import { pipe } from "effect/Function";
5
- export const syncError = (pathValue, reason) => ({
6
- _tag: "SyncError",
7
- path: pathValue,
8
- reason,
9
- });
10
- export const ensureDirectory = (directory) => Effect.try({
11
- try: () => {
12
- fs.mkdirSync(directory, { recursive: true });
13
- },
14
- catch: () => syncError(directory, "Cannot create destination directory structure"),
15
- });
16
- export const runSyncSource = (source, options) => pipe(Effect.gen(function* (_) {
17
- const resolvedSource = yield* _(source.resolveSource(options));
18
- const destination = path.join(options.cwd, ".knowledge", source.destSubdir);
19
- if (path.resolve(resolvedSource) === path.resolve(destination)) {
20
- yield* _(Console.log(`${source.name}: source equals destination; skipping copy to avoid duplicates`));
21
- return;
22
- }
23
- yield* _(ensureDirectory(destination));
24
- const copied = yield* _(source.copy(resolvedSource, destination, options));
25
- yield* _(Console.log(`${source.name}: copied ${copied} files from ${resolvedSource} to ${destination}`));
26
- }), Effect.catchAll((error) => Console.log(`${source.name}: source not found; skipped syncing (${error.reason})`)));
27
- export const copyFilteredFiles = (sourceRoot, destinationRoot, isRelevant, errorReason) => Effect.try({
28
- try: () => {
29
- let copied = 0;
30
- const walk = (current) => {
31
- const entries = fs.readdirSync(current, { withFileTypes: true });
32
- for (const entry of entries) {
33
- const full = path.join(current, entry.name);
34
- if (entry.isDirectory()) {
35
- walk(full);
36
- }
37
- else if (isRelevant(entry, full)) {
38
- const target = path.join(destinationRoot, path.relative(sourceRoot, full));
39
- fs.mkdirSync(path.dirname(target), { recursive: true });
40
- fs.copyFileSync(full, target);
41
- copied += 1;
42
- }
43
- }
44
- };
45
- walk(sourceRoot);
46
- return copied;
47
- },
48
- catch: () => syncError(sourceRoot, errorReason),
49
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,39 +0,0 @@
1
- import { Option } from "effect";
2
-
3
- export interface AppProfile {
4
- readonly name: string;
5
- readonly mission: string;
6
- }
7
-
8
- export interface Greeting {
9
- readonly message: string;
10
- }
11
-
12
- const normalize = (value: string): string => value.trim();
13
-
14
- const isNonEmpty = (value: string): boolean => value.length > 0;
15
-
16
- /**
17
- * CHANGE: Introduce pure greeting synthesis for console entrypoint
18
- * WHY: Provide mathematically checkable construction of startup banner without side effects
19
- * QUOTE(ТЗ): "Каждая функция — это теорема."
20
- * REF: REQ-GREETING-CORE
21
- * SOURCE: AGENTS.md – функциональное ядро без побочных эффектов
22
- * FORMAT THEOREM: ∀p ∈ AppProfile: valid(p) → nonEmpty(buildGreeting(p).message)
23
- * PURITY: CORE
24
- * EFFECT: None (pure)
25
- * INVARIANT: output.message.length > 0 when Option is Some
26
- * COMPLEXITY: O(1)/O(1)
27
- */
28
- export const buildGreeting = (profile: AppProfile): Option.Option<Greeting> => {
29
- const trimmedName = normalize(profile.name);
30
- const trimmedMission = normalize(profile.mission);
31
-
32
- if (!isNonEmpty(trimmedName) || !isNonEmpty(trimmedMission)) {
33
- return Option.none();
34
- }
35
-
36
- return Option.some({
37
- message: `${trimmedName}: ${trimmedMission}`,
38
- });
39
- };
package/src/main.ts DELETED
@@ -1,49 +0,0 @@
1
- import { Console, Effect, Option, pipe } from "effect";
2
- import { match } from "ts-pattern";
3
- import type { AppProfile } from "./core/greeting.js";
4
- import { buildGreeting } from "./core/greeting.js";
5
-
6
- interface StartupError {
7
- readonly _tag: "StartupError";
8
- readonly reason: string;
9
- }
10
-
11
- const appProfile: AppProfile = {
12
- name: "Context Console",
13
- mission: "Functional core ready for verifiable effects",
14
- };
15
-
16
- const toStartupError = (reason: string): StartupError => ({
17
- _tag: "StartupError",
18
- reason,
19
- });
20
-
21
- /**
22
- * CHANGE: Compose shell-level startup program around pure greeting builder
23
- * WHY: Isolate side effects (console IO) while delegating computation to functional core
24
- * QUOTE(ТЗ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
25
- * REF: REQ-SHELL-STARTUP
26
- * SOURCE: AGENTS.md — эффекты только в SHELL
27
- * FORMAT THEOREM: ∀profile: valid(profile) → logs(buildGreeting(profile))
28
- * PURITY: SHELL
29
- * EFFECT: Effect<void, StartupError, Console>
30
- * INVARIANT: Console side-effects occur once and only after successful greeting synthesis
31
- * COMPLEXITY: O(1)/O(1)
32
- */
33
- const program = pipe(
34
- Effect.succeed(buildGreeting(appProfile)),
35
- Effect.filterOrFail(Option.isSome, () =>
36
- toStartupError("Profile must not be empty"),
37
- ),
38
- Effect.map((option) => option.value),
39
- Effect.flatMap((greeting) => Console.log(greeting.message)),
40
- Effect.catchAll((error) =>
41
- match(error)
42
- .with({ _tag: "StartupError" }, (startupError) =>
43
- Console.error(`StartupError: ${startupError.reason}`),
44
- )
45
- .exhaustive(),
46
- ),
47
- );
48
-
49
- Effect.runSync(program);
@@ -1,55 +0,0 @@
1
- import * as NodeFs from "node:fs";
2
- import * as NodeOs from "node:os";
3
- import * as NodePath from "node:path";
4
- import { Effect, pipe } from "effect";
5
- import { copyFilteredFiles, runSyncSource, syncError } from "./syncShared.js";
6
- import type { SyncError, SyncOptions, SyncSource } from "./syncTypes.js";
7
-
8
- const slugFromCwd = (cwd: string): string =>
9
- `-${cwd.replace(/^\/+/, "").replace(/\//g, "-")}`;
10
-
11
- const resolveClaudeProjectDir = (
12
- cwd: string,
13
- overrideProjectsRoot?: string,
14
- ): Effect.Effect<string, SyncError> =>
15
- pipe(
16
- Effect.sync(() => {
17
- const slug = slugFromCwd(cwd);
18
- const base =
19
- overrideProjectsRoot ??
20
- NodePath.join(NodeOs.homedir(), ".claude", "projects");
21
- const candidate = NodePath.join(base, slug);
22
- return NodeFs.existsSync(candidate) ? candidate : undefined;
23
- }),
24
- Effect.flatMap((found) =>
25
- found === undefined
26
- ? Effect.fail(
27
- syncError(".claude", "Claude project directory is missing"),
28
- )
29
- : Effect.succeed(found),
30
- ),
31
- );
32
-
33
- const copyClaudeJsonl = (
34
- sourceDir: string,
35
- destinationDir: string,
36
- ): Effect.Effect<number, SyncError> =>
37
- copyFilteredFiles(
38
- sourceDir,
39
- destinationDir,
40
- (entry, fullPath) => entry.isFile() && fullPath.endsWith(".jsonl"),
41
- "Cannot traverse Claude project",
42
- );
43
-
44
- export const syncClaude = (
45
- options: SyncOptions,
46
- ): Effect.Effect<void, SyncError> => runSyncSource(claudeSource, options);
47
-
48
- const claudeSource: SyncSource = {
49
- name: "Claude",
50
- destSubdir: ".claude",
51
- resolveSource: (options) =>
52
- resolveClaudeProjectDir(options.cwd, options.claudeProjectsRoot),
53
- copy: (sourceDir, destinationDir) =>
54
- copyClaudeJsonl(sourceDir, destinationDir),
55
- };
@@ -1,236 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import * as S from "@effect/schema/Schema";
5
- import { Console, Effect, pipe } from "effect";
6
- import type { ProjectLocator } from "../core/knowledge.js";
7
- import { buildProjectLocator, linesMatchProject } from "../core/knowledge.js";
8
- import { ensureDirectory, syncError } from "./syncShared.js";
9
- import type { SyncError, SyncOptions } from "./syncTypes.js";
10
-
11
- const PackageRepositorySchema = S.Struct({
12
- url: S.String,
13
- });
14
-
15
- const PackageFileSchema = S.Struct({
16
- repository: S.optional(PackageRepositorySchema),
17
- });
18
- const PackageFileFromJson = S.compose(S.parseJson(), PackageFileSchema);
19
- const decodePackageFile = S.decodeUnknownSync(PackageFileFromJson);
20
-
21
- const readRepositoryUrl = (
22
- cwd: string,
23
- repositoryUrlOverride?: string,
24
- ): Effect.Effect<string, SyncError> =>
25
- Effect.try({
26
- try: () => {
27
- if (repositoryUrlOverride !== undefined) {
28
- return repositoryUrlOverride;
29
- }
30
-
31
- const raw = fs.readFileSync(path.join(cwd, "package.json"), "utf8");
32
- const parsed = decodePackageFile(raw);
33
- const repositoryUrl = parsed.repository?.url;
34
-
35
- if (repositoryUrl === undefined) {
36
- throw new Error("repository url is missing");
37
- }
38
-
39
- return repositoryUrl;
40
- },
41
- catch: () => syncError("package.json", "Cannot read repository url"),
42
- });
43
-
44
- const resolveSourceDir = (
45
- cwd: string,
46
- override?: string,
47
- metaRoot?: string,
48
- ): Effect.Effect<string, SyncError> =>
49
- pipe(
50
- Effect.sync(() => {
51
- const envSource = process.env.CODEX_SOURCE_DIR;
52
- const metaCandidate =
53
- metaRoot === undefined
54
- ? undefined
55
- : metaRoot.endsWith(".codex")
56
- ? metaRoot
57
- : path.join(metaRoot, ".codex");
58
- const localSource = path.join(cwd, ".codex");
59
- const homeSource = path.join(os.homedir(), ".codex");
60
- const localKnowledge = path.join(cwd, ".knowledge", ".codex");
61
- const homeKnowledge = path.join(os.homedir(), ".knowledge", ".codex");
62
-
63
- const candidates = [
64
- override,
65
- envSource,
66
- metaCandidate,
67
- localSource,
68
- homeSource,
69
- localKnowledge,
70
- homeKnowledge,
71
- ].filter((candidate): candidate is string => candidate !== undefined);
72
-
73
- const existing = candidates.find((candidate) => fs.existsSync(candidate));
74
-
75
- return { existing, candidates };
76
- }),
77
- Effect.flatMap(({ existing, candidates }) =>
78
- existing === undefined
79
- ? Effect.fail(
80
- syncError(
81
- ".codex",
82
- `Source .codex directory is missing; checked: ${candidates.join(", ")}`,
83
- ),
84
- )
85
- : Effect.succeed(existing),
86
- ),
87
- );
88
-
89
- const collectJsonlFiles = (
90
- root: string,
91
- ): Effect.Effect<ReadonlyArray<string>, SyncError> =>
92
- Effect.try({
93
- try: () => {
94
- const collected: string[] = [];
95
- const walk = (current: string): void => {
96
- const entries = fs.readdirSync(current, { withFileTypes: true });
97
- for (const entry of entries) {
98
- const fullPath = path.join(current, entry.name);
99
- if (entry.isDirectory()) {
100
- walk(fullPath);
101
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
102
- collected.push(fullPath);
103
- }
104
- }
105
- };
106
-
107
- walk(root);
108
- return collected;
109
- },
110
- catch: () => syncError(root, "Cannot traverse .codex"),
111
- });
112
-
113
- const isFileRelevant = (
114
- filePath: string,
115
- locator: ProjectLocator,
116
- ): Effect.Effect<boolean, SyncError> =>
117
- Effect.try({
118
- try: () => {
119
- const content = fs.readFileSync(filePath, "utf8");
120
- const lines = content.split("\n");
121
- return linesMatchProject(lines, locator);
122
- },
123
- catch: () => syncError(filePath, "Cannot read jsonl file"),
124
- });
125
-
126
- const copyRelevantFile = (
127
- sourceRoot: string,
128
- destinationRoot: string,
129
- filePath: string,
130
- ): Effect.Effect<void, SyncError> =>
131
- Effect.try({
132
- try: () => {
133
- const relative = path.relative(sourceRoot, filePath);
134
- const targetPath = path.join(destinationRoot, relative);
135
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
136
- fs.copyFileSync(filePath, targetPath);
137
- },
138
- catch: () => syncError(filePath, "Cannot copy file into .knowledge/.codex"),
139
- });
140
-
141
- const selectRelevantFiles = (
142
- files: ReadonlyArray<string>,
143
- locator: ProjectLocator,
144
- ): Effect.Effect<ReadonlyArray<string>, SyncError> =>
145
- Effect.reduce(files, [] as string[], (acc, filePath) =>
146
- pipe(
147
- isFileRelevant(filePath, locator),
148
- Effect.map((isRelevant) => {
149
- if (isRelevant) {
150
- acc.push(filePath);
151
- }
152
-
153
- return acc;
154
- }),
155
- ),
156
- );
157
-
158
- const resolveLocator = (
159
- options: SyncOptions,
160
- ): Effect.Effect<ProjectLocator, SyncError> =>
161
- pipe(
162
- readRepositoryUrl(options.cwd, options.repositoryUrlOverride),
163
- Effect.map((repositoryUrl) =>
164
- buildProjectLocator(repositoryUrl, options.cwd),
165
- ),
166
- Effect.catchAll(() =>
167
- Effect.gen(function* (__) {
168
- yield* __(
169
- Console.log(
170
- "Codex repository url missing; falling back to cwd-only match",
171
- ),
172
- );
173
- return buildProjectLocator(options.cwd, options.cwd);
174
- }),
175
- ),
176
- );
177
-
178
- const copyCodexFiles = (
179
- sourceDir: string,
180
- destinationDir: string,
181
- locator: ProjectLocator,
182
- ): Effect.Effect<void, SyncError> =>
183
- Effect.gen(function* (_) {
184
- yield* _(ensureDirectory(destinationDir));
185
- const allJsonlFiles = yield* _(collectJsonlFiles(sourceDir));
186
- const relevantFiles = yield* _(selectRelevantFiles(allJsonlFiles, locator));
187
- yield* _(
188
- Effect.forEach(relevantFiles, (filePath) =>
189
- copyRelevantFile(sourceDir, destinationDir, filePath),
190
- ),
191
- );
192
- yield* _(
193
- Console.log(
194
- `Codex: copied ${relevantFiles.length} files from ${sourceDir} to ${destinationDir}`,
195
- ),
196
- );
197
- });
198
-
199
- // CHANGE: Extract Codex dialog sync into dedicated module for clarity.
200
- // WHY: Separate Codex-specific shell effects from other sync flows.
201
- // QUOTE(ТЗ): "вынеси в отдельный файл"
202
- // REF: user request 2025-11-26
203
- // SOURCE: internal requirement
204
- // FORMAT THEOREM: ∀f ∈ Files: relevant(f, locator) → copied(f, destination)
205
- // PURITY: SHELL
206
- // EFFECT: Effect<void, SyncError, never>
207
- // INVARIANT: ∀f ∈ copiedFiles: linesMatchProject(f, locator)
208
- // COMPLEXITY: O(n) time / O(n) space, n = |files|
209
- export const syncCodex = (
210
- options: SyncOptions,
211
- ): Effect.Effect<void, SyncError> =>
212
- Effect.gen(function* (_) {
213
- const locator = yield* _(resolveLocator(options));
214
- const sourceDir = yield* _(
215
- resolveSourceDir(options.cwd, options.sourceDir, options.metaRoot),
216
- );
217
- const destinationDir =
218
- options.destinationDir ?? path.join(options.cwd, ".knowledge", ".codex");
219
-
220
- if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
221
- yield* _(
222
- Console.log(
223
- "Codex source equals destination; skipping copy to avoid duplicates",
224
- ),
225
- );
226
- return;
227
- }
228
-
229
- yield* _(copyCodexFiles(sourceDir, destinationDir, locator));
230
- }).pipe(
231
- Effect.catchAll((error) =>
232
- Console.log(
233
- `Codex source not found; skipped syncing Codex dialog files (${error.reason})`,
234
- ),
235
- ),
236
- );