@prover-coder-ai/context-doc 1.0.11 → 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 -129
  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 -54
  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
@@ -0,0 +1,203 @@
1
+ import { NodeContext } from "@effect/platform-node"
2
+ import type * as FileSystem from "@effect/platform/FileSystem"
3
+ import * as Path from "@effect/platform/Path"
4
+ import { describe, expect, it } from "@effect/vitest"
5
+ import { Effect, Layer, Option, pipe } from "effect"
6
+ import fc from "fast-check"
7
+
8
+ import { CryptoService, CryptoServiceLive } from "../../src/shell/services/crypto.js"
9
+ import { FileSystemLive } from "../../src/shell/services/file-system.js"
10
+ import { RuntimeEnv } from "../../src/shell/services/runtime-env.js"
11
+ import { buildSyncProgram } from "../../src/shell/sync/index.js"
12
+ import { buildTestPaths, makeTempDir, withFsPath, writeFile } from "../support/fs-helpers.js"
13
+
14
+ const forEach = Effect.forEach
15
+ const some = Option.some
16
+ const mapFileEntry = (
17
+ sessionDir: string,
18
+ fs: FileSystem.FileSystem,
19
+ path: Path.Path
20
+ ) =>
21
+ (entry: string) =>
22
+ pipe(
23
+ fs.stat(path.join(sessionDir, entry)),
24
+ Effect.map((info) => info.type === "File" ? some(entry) : Option.none())
25
+ )
26
+
27
+ const testPaths = buildTestPaths(
28
+ new URL(import.meta.url),
29
+ "context-doc-tests"
30
+ )
31
+
32
+ const withTempDir = Effect.gen(function*(_) {
33
+ const { tempBase } = yield* _(testPaths)
34
+ return yield* _(makeTempDir(tempBase, "context-doc-"))
35
+ })
36
+
37
+ const makeRuntimeEnvLayer = (cwd: string): Layer.Layer<RuntimeEnv> =>
38
+ Layer.succeed(RuntimeEnv, {
39
+ argv: Effect.succeed(["node", "main"]),
40
+ cwd: Effect.succeed(cwd),
41
+ homedir: Effect.succeed(cwd),
42
+ envVar: () => Effect.succeed(Option.none())
43
+ })
44
+
45
+ const assertSyncOutput = (
46
+ destDir: string,
47
+ qwenHash: string,
48
+ cwd: string,
49
+ expectedMessage: string,
50
+ skippedMessage: string
51
+ ) =>
52
+ withFsPath((fs, path) =>
53
+ Effect.gen(function*(_) {
54
+ const sessionDir = path.join(destDir, "sessions/2025/11")
55
+ const entries = yield* _(fs.readDirectory(sessionDir))
56
+ const files = yield* _(
57
+ forEach(entries, mapFileEntry(sessionDir, fs, path))
58
+ )
59
+ const copiedFiles = files.flatMap((entry) => Option.isSome(entry) ? [entry.value] : [])
60
+
61
+ expect(copiedFiles).toEqual(["match.jsonl"])
62
+
63
+ const content = yield* _(
64
+ fs.readFileString(path.join(sessionDir, "match.jsonl"))
65
+ )
66
+
67
+ expect(content).toContain(`"message":"${expectedMessage}"`)
68
+ expect(content).not.toContain(`"message":"${skippedMessage}"`)
69
+
70
+ const qwenCopied = path.join(
71
+ cwd,
72
+ ".knowledge",
73
+ ".qwen",
74
+ qwenHash,
75
+ "chats",
76
+ "session-1.json"
77
+ )
78
+
79
+ const exists = yield* _(fs.exists(qwenCopied))
80
+ expect(exists).toBe(true)
81
+ })
82
+ )
83
+
84
+ const runSyncScenario = (
85
+ matchMessage: string,
86
+ skippedMessage: string
87
+ ) =>
88
+ Effect.scoped(
89
+ Effect.gen(function*(_) {
90
+ const path = yield* _(Path.Path)
91
+ const crypto = yield* _(CryptoService)
92
+ const cwd = yield* _(withTempDir)
93
+ const codexDir = path.join(cwd, ".codex")
94
+ const destDir = path.join(cwd, ".knowledge", ".codex")
95
+ const qwenSource = path.join(cwd, ".qwen", "tmp")
96
+ const qwenHash = yield* _(crypto.sha256(cwd))
97
+ yield* _(
98
+ writeFile(
99
+ path.join(codexDir, "sessions/2025/11/match.jsonl"),
100
+ [
101
+ JSON.stringify({ cwd, message: matchMessage }),
102
+ JSON.stringify({
103
+ payload: { cwd: path.join(cwd, "sub") }
104
+ })
105
+ ].join("\n")
106
+ )
107
+ )
108
+
109
+ yield* _(
110
+ writeFile(
111
+ path.join(codexDir, "sessions/2025/11/ignore.jsonl"),
112
+ [
113
+ JSON.stringify({
114
+ cwd: "/home/user/other",
115
+ message: skippedMessage
116
+ })
117
+ ].join("\n")
118
+ )
119
+ )
120
+
121
+ yield* _(
122
+ writeFile(
123
+ path.join(qwenSource, qwenHash, "chats", "session-1.json"),
124
+ JSON.stringify({ sessionId: "s1", projectHash: qwenHash })
125
+ )
126
+ )
127
+
128
+ yield* _(
129
+ Effect.provide(
130
+ buildSyncProgram({
131
+ cwd,
132
+ sourceDir: codexDir,
133
+ destinationDir: destDir,
134
+ qwenSourceDir: qwenSource
135
+ }),
136
+ Layer.mergeAll(FileSystemLive, makeRuntimeEnvLayer(cwd))
137
+ )
138
+ )
139
+
140
+ yield* _(assertSyncOutput(destDir, qwenHash, cwd, matchMessage, skippedMessage))
141
+ })
142
+ ).pipe(
143
+ Effect.provide(Layer.mergeAll(NodeContext.layer, CryptoServiceLive))
144
+ )
145
+
146
+ describe("sync-knowledge end-to-end", () => {
147
+ const safeChar = fc.constantFrom(
148
+ "a",
149
+ "b",
150
+ "c",
151
+ "d",
152
+ "e",
153
+ "f",
154
+ "g",
155
+ "h",
156
+ "i",
157
+ "j",
158
+ "k",
159
+ "l",
160
+ "m",
161
+ "n",
162
+ "o",
163
+ "p",
164
+ "q",
165
+ "r",
166
+ "s",
167
+ "t",
168
+ "u",
169
+ "v",
170
+ "w",
171
+ "x",
172
+ "y",
173
+ "z",
174
+ "0",
175
+ "1",
176
+ "2",
177
+ "3",
178
+ "4",
179
+ "5"
180
+ )
181
+ const message: fc.Arbitrary<string> = fc.string({
182
+ minLength: 1,
183
+ maxLength: 12,
184
+ unit: safeChar
185
+ })
186
+ const messagePair: fc.Arbitrary<[string, string]> = fc
187
+ .tuple(message, message)
188
+ .filter((pair) => pair[0] !== pair[1])
189
+
190
+ it.effect("copies matching Codex files and Qwen json", () =>
191
+ Effect.tryPromise({
192
+ try: () =>
193
+ fc.assert(
194
+ fc.asyncProperty(
195
+ messagePair,
196
+ ([matchMessage, skippedMessage]: [string, string]) =>
197
+ Effect.runPromise(runSyncScenario(matchMessage, skippedMessage))
198
+ ),
199
+ { numRuns: 5 }
200
+ ),
201
+ catch: (error) => error instanceof Error ? error : new Error(String(error))
202
+ }))
203
+ })
@@ -0,0 +1,50 @@
1
+ import * as FileSystem from "@effect/platform/FileSystem"
2
+ import * as Path from "@effect/platform/Path"
3
+ import { Effect, pipe } from "effect"
4
+
5
+ const accessFsPath = Effect.gen(function*(_) {
6
+ const fs = yield* _(FileSystem.FileSystem)
7
+ const path = yield* _(Path.Path)
8
+ return { fs, path }
9
+ })
10
+
11
+ export const withFsPath = <A, E, R>(
12
+ fn: (fs: FileSystem.FileSystem, path: Path.Path) => Effect.Effect<A, E, R>
13
+ ): Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | R> =>
14
+ pipe(
15
+ accessFsPath,
16
+ Effect.flatMap(({ fs, path }) => fn(fs, path))
17
+ )
18
+
19
+ export const writeFile = (filePath: string, content: string) =>
20
+ withFsPath((fs, path) =>
21
+ Effect.gen(function*(_) {
22
+ yield* _(fs.makeDirectory(path.dirname(filePath), { recursive: true }))
23
+ yield* _(fs.writeFileString(filePath, content))
24
+ })
25
+ )
26
+
27
+ export const makeTempDir = (base: string, prefix: string) =>
28
+ withFsPath((fs, _path) =>
29
+ Effect.gen(function*(_) {
30
+ yield* _(fs.makeDirectory(base, { recursive: true }))
31
+ return yield* _(fs.makeTempDirectoryScoped({ directory: base, prefix }))
32
+ })
33
+ )
34
+
35
+ export const buildTestPaths = (
36
+ testUrl: URL,
37
+ tempName: string,
38
+ packName?: string
39
+ ) =>
40
+ Effect.gen(function*(_) {
41
+ const path = yield* _(Path.Path)
42
+ const testFile = yield* _(path.fromFileUrl(testUrl))
43
+ const testRoot = path.dirname(testFile)
44
+ const projectRoot = path.resolve(testRoot, "../../../..")
45
+ const tempBase = path.join(projectRoot, ".tmp", tempName)
46
+ const packBase = packName === undefined
47
+ ? undefined
48
+ : path.join(projectRoot, ".tmp", packName)
49
+ return { projectRoot, tempBase, packBase }
50
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "dist",
6
+ "types": ["vitest"],
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["src/*"]
10
+ }
11
+ },
12
+ "include": [
13
+ "src/**/*",
14
+ "tests/**/*",
15
+ "vite.config.ts",
16
+ "vitest.config.ts"
17
+ ],
18
+ "exclude": ["dist", "node_modules"]
19
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,32 @@
1
+ import path from "node:path"
2
+ import { fileURLToPath } from "node:url"
3
+ import { defineConfig } from "vite"
4
+ import tsconfigPaths from "vite-tsconfig-paths"
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+
9
+ export default defineConfig({
10
+ plugins: [tsconfigPaths()],
11
+ publicDir: false,
12
+ resolve: {
13
+ alias: {
14
+ "@": path.resolve(__dirname, "src")
15
+ }
16
+ },
17
+ build: {
18
+ target: "node20",
19
+ outDir: "dist",
20
+ sourcemap: true,
21
+ ssr: "src/app/main.ts",
22
+ rollupOptions: {
23
+ output: {
24
+ format: "es",
25
+ entryFileNames: "main.js"
26
+ }
27
+ }
28
+ },
29
+ ssr: {
30
+ target: "node"
31
+ }
32
+ })
package/vitest.config.ts CHANGED
@@ -7,75 +7,79 @@
7
7
  // EFFECT: Effect<TestReport, never, TestEnvironment>
8
8
  // COMPLEXITY: O(n) test execution where n = |test_files|
9
9
 
10
- import { defineConfig } from "vitest/config";
11
- import tsconfigPaths from "vite-tsconfig-paths";
10
+ import path from "node:path"
11
+ import { fileURLToPath } from "node:url"
12
+ import tsconfigPaths from "vite-tsconfig-paths"
13
+ import { defineConfig } from "vitest/config"
12
14
 
13
- export default defineConfig({
14
- plugins: [tsconfigPaths()], // Resolves @/* paths from tsconfig
15
- test: {
16
- // CHANGE: Native ESM support without experimental flags
17
- // WHY: Vitest designed for ESM, no need for --experimental-vm-modules
18
- // INVARIANT: Deterministic test execution without side effects
19
- globals: false, // IMPORTANT: Use explicit imports for type safety
20
- environment: "node",
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
21
17
 
22
- // CHANGE: Match Jest's test file patterns
23
- // INVARIANT: Same test discovery as Jest
24
- include: ["tests/**/*.{test,spec}.ts"],
25
- exclude: ["node_modules", "dist", "dist-test"],
18
+ export default defineConfig({
19
+ plugins: [tsconfigPaths()], // Resolves @/* paths from tsconfig
20
+ test: {
21
+ // CHANGE: Native ESM support without experimental flags
22
+ // WHY: Vitest designed for ESM, no need for --experimental-vm-modules
23
+ // INVARIANT: Deterministic test execution without side effects
24
+ globals: false, // IMPORTANT: Use explicit imports for type safety
25
+ environment: "node",
26
26
 
27
- // CHANGE: Coverage with 100% threshold for CORE (same as Jest)
28
- // WHY: CORE must maintain mathematical guarantees via complete coverage
29
- // INVARIANT: coverage_vitest ≥ coverage_jest ∧ ∀ f ∈ CORE: coverage(f) = 100%
30
- coverage: {
31
- provider: "v8", // Faster than babel (istanbul), native V8 coverage
32
- reporter: ["text", "json", "html"],
33
- include: ["src/**/*.ts"],
34
- exclude: [
35
- "src/**/*.test.ts",
36
- "src/**/*.spec.ts",
37
- "src/**/__tests__/**",
38
- "scripts/**/*.ts",
39
- ],
40
- // CHANGE: Maintain exact same thresholds as Jest
41
- // WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
42
- // INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
43
- // NOTE: Vitest v8 provider collects coverage for all matched files by default
44
- thresholds: {
45
- "src/core/**/*.ts": {
46
- branches: 100,
47
- functions: 100,
48
- lines: 100,
49
- statements: 100,
50
- },
51
- global: {
52
- branches: 10,
53
- functions: 10,
54
- lines: 10,
55
- statements: 10,
56
- },
57
- },
58
- },
27
+ // CHANGE: Match Jest's test file patterns
28
+ // INVARIANT: Same test discovery as Jest
29
+ include: ["tests/**/*.{test,spec}.ts"],
30
+ exclude: ["node_modules", "dist", "dist-test"],
59
31
 
60
- // CHANGE: Faster test execution via thread pooling
61
- // WHY: Vitest uses worker threads by default (faster than Jest's processes)
62
- // COMPLEXITY: O(n/k) where n = tests, k = worker_count
63
- // NOTE: Vitest runs tests in parallel by default, no additional config needed
32
+ // CHANGE: Coverage with 100% threshold for CORE (same as Jest)
33
+ // WHY: CORE must maintain mathematical guarantees via complete coverage
34
+ // INVARIANT: coverage_vitest coverage_jest f ∈ CORE: coverage(f) = 100%
35
+ coverage: {
36
+ provider: "v8", // Faster than babel (istanbul), native V8 coverage
37
+ reporter: ["text", "json", "html"],
38
+ include: ["src/**/*.ts"],
39
+ exclude: [
40
+ "src/**/*.test.ts",
41
+ "src/**/*.spec.ts",
42
+ "src/**/__tests__/**",
43
+ "scripts/**/*.ts"
44
+ ],
45
+ // CHANGE: Maintain exact same thresholds as Jest
46
+ // WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
47
+ // INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
48
+ // NOTE: Vitest v8 provider collects coverage for all matched files by default
49
+ thresholds: {
50
+ "src/core/**/*.ts": {
51
+ branches: 100,
52
+ functions: 100,
53
+ lines: 100,
54
+ statements: 100
55
+ },
56
+ global: {
57
+ branches: 10,
58
+ functions: 10,
59
+ lines: 10,
60
+ statements: 10
61
+ }
62
+ }
63
+ },
64
64
 
65
- // CHANGE: Clear mocks between tests (Jest equivalence)
66
- // WHY: Prevent test contamination, ensure test independence
67
- // INVARIANT: test_i, test_j: independent(test_i, test_j) no_shared_state
68
- clearMocks: true,
69
- mockReset: true,
70
- restoreMocks: true,
65
+ // CHANGE: Faster test execution via thread pooling
66
+ // WHY: Vitest uses worker threads by default (faster than Jest's processes)
67
+ // COMPLEXITY: O(n/k) where n = tests, k = worker_count
68
+ // NOTE: Vitest runs tests in parallel by default, no additional config needed
71
69
 
72
- // CHANGE: Disable globals to enforce explicit imports
73
- // WHY: Type safety, explicit dependencies, functional purity
74
- // NOTE: Tests must import { describe, it, expect } from "vitest"
75
- },
76
- resolve: {
77
- alias: {
78
- "@": "/src",
79
- },
80
- },
81
- });
70
+ // CHANGE: Clear mocks between tests (Jest equivalence)
71
+ // WHY: Prevent test contamination, ensure test independence
72
+ // INVARIANT: test_i, test_j: independent(test_i, test_j) no_shared_state
73
+ clearMocks: true,
74
+ mockReset: true,
75
+ restoreMocks: true
76
+ // CHANGE: Disable globals to enforce explicit imports
77
+ // WHY: Type safety, explicit dependencies, functional purity
78
+ // NOTE: Tests must import { describe, it, expect } from "vitest"
79
+ },
80
+ resolve: {
81
+ alias: {
82
+ "@": path.resolve(__dirname, "src")
83
+ }
84
+ }
85
+ })
package/README.md DELETED
@@ -1,42 +0,0 @@
1
- # Knowledge Sync CLI
2
-
3
- Command: `npx @prover-coder-ai/context-doc [flags]` (or `context-doc` when installed globally) — copies project dialogs into `.knowledge` for both Codex and Qwen.
4
-
5
- ## Installation (global CLI)
6
- - Install: `npm install -g @prover-coder-ai/context-doc`
7
- - Registry page: https://www.npmjs.com/package/@prover-coder-ai/context-doc
8
- - Run globally: `context-doc [flags]`
9
- - Ad-hoc (no install): `npx @prover-coder-ai/context-doc [flags]`
10
- - Local (without global install): `npm run sync:knowledge` or `tsx src/shell/syncKnowledge.ts`
11
-
12
- ## Usage examples
13
- - Default (auto-detect sources/destinations): `context-doc`
14
- - Custom Codex source/destination: `context-doc --source /tmp/project/.codex --dest /tmp/project/.knowledge/.codex`
15
- - Custom meta root (both Codex/Qwen resolution): `context-doc --meta-root /data/meta`
16
- - Override project URL: `context-doc --project-url https://github.com/your/repo`
17
- - Qwen source override: `context-doc --qwen-source /data/qwen/tmp/abcd1234`
18
-
19
- ## Flags
20
- - `--source, -s <path>` — explicit path to `.codex` (Codex source).
21
- - `--dest, -d <path>` — Codex destination root (defaults to `.knowledge/.codex`).
22
- - `--project-url` / `--project-name <url>` — override repository URL (otherwise read from `package.json`).
23
- - `--meta-root <path>` — meta folder root; Codex lookup tries `<meta-root>/.codex`, Qwen lookup tries `<meta-root>/.qwen/tmp/<hash>`.
24
- - `--qwen-source <path>` — explicit path to Qwen source.
25
-
26
- ## Codex lookup order (`.jsonl` filtered by project)
27
- 1) `--source`
28
- 2) env `CODEX_SOURCE_DIR`
29
- 3) `--meta-root` (use if already `.codex` or append `/.codex`)
30
- 4) project-local `.codex`
31
- 5) home `~/.codex`
32
-
33
- Files: copies only `.jsonl` whose `git.repository_url` or `cwd` match this project into `.knowledge/.codex`, preserving structure.
34
-
35
- ## Qwen lookup order (`.json` only, no project filter)
36
- 1) `--qwen-source`
37
- 2) env `QWEN_SOURCE_DIR`
38
- 3) `<meta-root>/.qwen/tmp/<hash>`
39
- 4) `<cwd>/.qwen/tmp/<hash>`
40
- 5) `~/.qwen/tmp/<hash>`
41
-
42
- Files: copies only `.json` into `.knowledge/.qwen`, preserving structure (directories recreated).
@@ -1,25 +0,0 @@
1
- import { Option } from "effect";
2
- const normalize = (value) => value.trim();
3
- const isNonEmpty = (value) => value.length > 0;
4
- /**
5
- * CHANGE: Introduce pure greeting synthesis for console entrypoint
6
- * WHY: Provide mathematically checkable construction of startup banner without side effects
7
- * QUOTE(ТЗ): "Каждая функция — это теорема."
8
- * REF: REQ-GREETING-CORE
9
- * SOURCE: AGENTS.md – функциональное ядро без побочных эффектов
10
- * FORMAT THEOREM: ∀p ∈ AppProfile: valid(p) → nonEmpty(buildGreeting(p).message)
11
- * PURITY: CORE
12
- * EFFECT: None (pure)
13
- * INVARIANT: output.message.length > 0 when Option is Some
14
- * COMPLEXITY: O(1)/O(1)
15
- */
16
- export const buildGreeting = (profile) => {
17
- const trimmedName = normalize(profile.name);
18
- const trimmedMission = normalize(profile.mission);
19
- if (!isNonEmpty(trimmedName) || !isNonEmpty(trimmedMission)) {
20
- return Option.none();
21
- }
22
- return Option.some({
23
- message: `${trimmedName}: ${trimmedMission}`,
24
- });
25
- };
@@ -1,54 +0,0 @@
1
- import path from "node:path";
2
- import { Option, pipe } from "effect";
3
- const isJsonRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
- const normalizeRepositoryUrl = (value) => pipe(value.trim(), (trimmed) => trimmed.replace(/^git\+/, ""), (withoutPrefix) => withoutPrefix.replace(/\.git$/, ""), (withoutGitSuffix) => withoutGitSuffix.replace(/^git@github\.com:/, "https://github.com/"), (withoutSsh) => withoutSsh.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/"), (normalized) => normalized.toLowerCase());
5
- const normalizeCwd = (value) => path.resolve(value);
6
- const pickString = (record, key) => {
7
- const candidate = record[key];
8
- return typeof candidate === "string" ? Option.some(candidate) : Option.none();
9
- };
10
- const pickRecord = (record, key) => pipe(record[key], Option.fromNullable, Option.filter(isJsonRecord));
11
- const pickGitRepository = (record) => pipe(pickString(record, "repository_url"), Option.orElse(() => pickString(record, "repositoryUrl")));
12
- const extractRepository = (record) => pipe(pickRecord(record, "git"), Option.flatMap(pickGitRepository), Option.orElse(() => pipe(pickRecord(record, "payload"), Option.flatMap((payload) => pipe(pickRecord(payload, "git"), Option.flatMap(pickGitRepository))))));
13
- const extractCwd = (record) => pipe(pickString(record, "cwd"), Option.orElse(() => pipe(pickRecord(record, "payload"), Option.flatMap((payload) => pickString(payload, "cwd")))));
14
- const toMetadata = (value) => {
15
- if (!isJsonRecord(value)) {
16
- return { repositoryUrl: Option.none(), cwd: Option.none() };
17
- }
18
- return {
19
- repositoryUrl: extractRepository(value),
20
- cwd: extractCwd(value),
21
- };
22
- };
23
- const normalizeLocator = (repositoryUrl, cwd) => ({
24
- normalizedRepositoryUrl: normalizeRepositoryUrl(repositoryUrl),
25
- normalizedCwd: normalizeCwd(cwd),
26
- });
27
- const safeParseJson = (line) => {
28
- try {
29
- return Option.some(JSON.parse(line));
30
- }
31
- catch {
32
- return Option.none();
33
- }
34
- };
35
- const metadataMatches = (metadata, locator) => {
36
- const fallbackCwdOnly = locator.normalizedRepositoryUrl === locator.normalizedCwd;
37
- const repoMatches = Option.exists(metadata.repositoryUrl, (repositoryUrl) => normalizeRepositoryUrl(repositoryUrl) === locator.normalizedRepositoryUrl);
38
- const cwdMatches = Option.exists(metadata.cwd, (cwdValue) => {
39
- const normalized = normalizeCwd(cwdValue);
40
- return fallbackCwdOnly
41
- ? normalized === locator.normalizedCwd
42
- : locator.normalizedCwd.startsWith(normalized) ||
43
- normalized.startsWith(locator.normalizedCwd);
44
- });
45
- return repoMatches || cwdMatches;
46
- };
47
- export const buildProjectLocator = (repositoryUrl, cwd) => normalizeLocator(repositoryUrl, cwd);
48
- export const linesMatchProject = (lines, locator) => lines.some((line) => {
49
- const trimmed = line.trim();
50
- if (trimmed.length === 0) {
51
- return false;
52
- }
53
- return pipe(safeParseJson(trimmed), Option.map(toMetadata), Option.exists((metadata) => metadataMatches(metadata, locator)));
54
- });
package/dist/main.js DELETED
@@ -1,27 +0,0 @@
1
- import { Console, Effect, Option, pipe } from "effect";
2
- import { match } from "ts-pattern";
3
- import { buildGreeting } from "./core/greeting.js";
4
- const appProfile = {
5
- name: "Context Console",
6
- mission: "Functional core ready for verifiable effects",
7
- };
8
- const toStartupError = (reason) => ({
9
- _tag: "StartupError",
10
- reason,
11
- });
12
- /**
13
- * CHANGE: Compose shell-level startup program around pure greeting builder
14
- * WHY: Isolate side effects (console IO) while delegating computation to functional core
15
- * QUOTE(ТЗ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
16
- * REF: REQ-SHELL-STARTUP
17
- * SOURCE: AGENTS.md — эффекты только в SHELL
18
- * FORMAT THEOREM: ∀profile: valid(profile) → logs(buildGreeting(profile))
19
- * PURITY: SHELL
20
- * EFFECT: Effect<void, StartupError, Console>
21
- * INVARIANT: Console side-effects occur once and only after successful greeting synthesis
22
- * COMPLEXITY: O(1)/O(1)
23
- */
24
- const program = pipe(Effect.succeed(buildGreeting(appProfile)), Effect.filterOrFail(Option.isSome, () => toStartupError("Profile must not be empty")), Effect.map((option) => option.value), Effect.flatMap((greeting) => Console.log(greeting.message)), Effect.catchAll((error) => match(error)
25
- .with({ _tag: "StartupError" }, (startupError) => Console.error(`StartupError: ${startupError.reason}`))
26
- .exhaustive()));
27
- Effect.runSync(program);
@@ -1,23 +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
- const slugFromCwd = (cwd) => `-${cwd.replace(/^\/+/, "").replace(/\//g, "-")}`;
7
- const resolveClaudeProjectDir = (cwd, overrideProjectsRoot) => pipe(Effect.sync(() => {
8
- const slug = slugFromCwd(cwd);
9
- const base = overrideProjectsRoot ??
10
- NodePath.join(NodeOs.homedir(), ".claude", "projects");
11
- const candidate = NodePath.join(base, slug);
12
- return NodeFs.existsSync(candidate) ? candidate : undefined;
13
- }), Effect.flatMap((found) => found === undefined
14
- ? Effect.fail(syncError(".claude", "Claude project directory is missing"))
15
- : Effect.succeed(found)));
16
- const copyClaudeJsonl = (sourceDir, destinationDir) => copyFilteredFiles(sourceDir, destinationDir, (entry, fullPath) => entry.isFile() && fullPath.endsWith(".jsonl"), "Cannot traverse Claude project");
17
- export const syncClaude = (options) => runSyncSource(claudeSource, options);
18
- const claudeSource = {
19
- name: "Claude",
20
- destSubdir: ".claude",
21
- resolveSource: (options) => resolveClaudeProjectDir(options.cwd, options.claudeProjectsRoot),
22
- copy: (sourceDir, destinationDir) => copyClaudeJsonl(sourceDir, destinationDir),
23
- };
package/dist/shell/cli.js DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Effect } from "effect";
3
- import { buildSyncProgram, parseArgs } from "./syncKnowledge.js";
4
- const program = buildSyncProgram(parseArgs());
5
- Effect.runSync(program);