@chamba/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chamba contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @chamba/core
2
+
3
+ Pure core logic for [chamba](https://github.com/thelord07/chamba): workspace scanner,
4
+ plan generator + heuristic reviewer, git worktree manager, Obsidian context/vault
5
+ writer, and a filesystem memory store.
6
+
7
+ - **No Node APIs directly** — all OS access goes through ports (`FilesystemPort`,
8
+ `ProcessPort`, `ClockPort`), so it's testable and runtime-agnostic.
9
+ - **No LLM** — chamba never calls a model.
10
+ - Node implementations of the ports live in
11
+ [`@chamba/adapters`](https://www.npmjs.com/package/@chamba/adapters).
12
+
13
+ Most users want the [`@chamba/mcp`](https://www.npmjs.com/package/@chamba/mcp) server,
14
+ not this library directly.
15
+
16
+ ## License
17
+
18
+ MIT
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Time behind a port so date-dependent logic (plan filenames, vault notes) is
3
+ * testable and `@chamba/core` stays free of ambient `Date` calls.
4
+ */
5
+ interface ClockPort {
6
+ now(): Date;
7
+ /** Current date as `YYYY-MM-DD`. */
8
+ today(): string;
9
+ }
10
+
11
+ /** A single entry returned when listing a directory. */
12
+ interface DirEntry {
13
+ name: string;
14
+ isDirectory: boolean;
15
+ isFile: boolean;
16
+ }
17
+ /**
18
+ * Filesystem access behind a port so `@chamba/core` never imports `node:fs`.
19
+ * Node implementation lives in `@chamba/adapters`; an in-memory implementation
20
+ * for tests lives in `@chamba/core/testing`.
21
+ */
22
+ interface FilesystemPort {
23
+ readFile(path: string): Promise<string>;
24
+ writeFile(path: string, content: string): Promise<void>;
25
+ readDir(path: string): Promise<DirEntry[]>;
26
+ exists(path: string): Promise<boolean>;
27
+ /** Create a directory and any missing parents (recursive). */
28
+ mkdir(path: string): Promise<void>;
29
+ /** Remove a file or directory (recursive); a no-op if it doesn't exist. */
30
+ remove(path: string): Promise<void>;
31
+ }
32
+
33
+ interface Memory {
34
+ key: string;
35
+ content: string;
36
+ tags: string[];
37
+ /** ISO timestamp of first write. */
38
+ createdAt: string;
39
+ /** ISO timestamp of last write (set when the memory is appended to). */
40
+ updatedAt: string;
41
+ /** Path to the markdown file on disk. */
42
+ path: string;
43
+ }
44
+ interface RememberInput {
45
+ key: string;
46
+ content: string;
47
+ tags?: string[];
48
+ }
49
+ /**
50
+ * Persistent, human-editable memory across sessions. Implementations store one
51
+ * markdown file per memory (never JSON or a DB), so the user can read and edit
52
+ * them by hand.
53
+ */
54
+ interface MemoryStore {
55
+ remember(input: RememberInput): Promise<Memory>;
56
+ recall(query: string): Promise<Memory[]>;
57
+ }
58
+
59
+ /** Directory (relative to root) where memories live. */
60
+ declare const MEMORY_DIR = ".chamba/memory";
61
+ /**
62
+ * Filesystem-backed `MemoryStore`. One markdown file per memory under
63
+ * `.chamba/memory/<slug>.md`, with YAML-ish frontmatter (key, tags, createdAt,
64
+ * updatedAt). Re-remembering an existing key appends a timestamped section
65
+ * instead of overwriting. Search is case-insensitive substring over key, tags
66
+ * and content.
67
+ */
68
+ declare class FilesystemMemoryStore implements MemoryStore {
69
+ private readonly fs;
70
+ private readonly clock;
71
+ private readonly root;
72
+ constructor(fs: FilesystemPort, clock: ClockPort, root: string);
73
+ private dir;
74
+ private pathFor;
75
+ remember(input: RememberInput): Promise<Memory>;
76
+ recall(query: string): Promise<Memory[]>;
77
+ private read;
78
+ }
79
+
80
+ interface NoteFields {
81
+ title: string;
82
+ /** `YYYY-MM-DD`. */
83
+ date: string;
84
+ tags: string[];
85
+ /** Markdown body the model produced (summary, plan, decisions, etc.). */
86
+ body: string;
87
+ }
88
+ /** Turn a title into a filesystem- and Obsidian-friendly slug. */
89
+ declare function slugify(title: string): string;
90
+ /**
91
+ * Render a vault note: valid YAML frontmatter (parseable by Obsidian) followed
92
+ * by the model's markdown body. Override this template by passing your own body
93
+ * structure — chamba only owns the frontmatter and the title heading.
94
+ */
95
+ declare function renderNote(fields: NoteFields): string;
96
+
97
+ interface WriteNoteInput {
98
+ vaultPath: string;
99
+ title: string;
100
+ content: string;
101
+ /** Subfolder slug under `proyectos/`; defaults to the title slug. */
102
+ projectSlug?: string;
103
+ tags?: string[];
104
+ }
105
+ interface WriteNoteResult {
106
+ notePath: string;
107
+ }
108
+ /** Subfolder inside the vault where chamba writes its summaries. */
109
+ declare const VAULT_NOTES_DIR = "proyectos";
110
+ /**
111
+ * Write a structured summary note into an Obsidian vault at
112
+ * `<vault>/proyectos/<date>-<slug>.md` with valid YAML frontmatter.
113
+ */
114
+ declare class VaultWriter {
115
+ private readonly fs;
116
+ private readonly clock;
117
+ constructor(fs: FilesystemPort, clock: ClockPort);
118
+ write(input: WriteNoteInput): Promise<WriteNoteResult>;
119
+ }
120
+
121
+ /** Relative path (from workspace root) of the chamba workspace file. */
122
+ declare const WORKSPACE_DIR = ".chamba";
123
+ declare const WORKSPACE_FILE = "workspace.md";
124
+ declare const WORKSPACE_RELATIVE_PATH = ".chamba/workspace.md";
125
+ interface ProjectRef {
126
+ name: string;
127
+ /** Path relative to the workspace root; `.` for the root project. */
128
+ path: string;
129
+ language?: string;
130
+ framework?: string;
131
+ }
132
+ interface Workspace {
133
+ root: string;
134
+ description: string;
135
+ languages: string[];
136
+ framework?: string;
137
+ conventions: string[];
138
+ projects: ProjectRef[];
139
+ /** Top-level directory names (without trailing slash), sorted. */
140
+ folderMap: string[];
141
+ }
142
+ /**
143
+ * Render a `Workspace` to the markdown that gets written to
144
+ * `.chamba/workspace.md`. Deterministic on purpose — no timestamps — so that
145
+ * `workspace_reload` produces meaningful diffs instead of churn.
146
+ */
147
+ declare function renderWorkspaceMarkdown(ws: Workspace): string;
148
+
149
+ type IssueSeverity = 'error' | 'warning';
150
+ interface Issue {
151
+ code: string;
152
+ severity: IssueSeverity;
153
+ message: string;
154
+ }
155
+ interface ValidatePlanInput {
156
+ plan: string;
157
+ task: string;
158
+ context?: string;
159
+ workspace?: Workspace;
160
+ }
161
+ interface ValidationResult {
162
+ issues: Issue[];
163
+ suggestions: string[];
164
+ riskFlags: string[];
165
+ }
166
+ /**
167
+ * Validate a plan with programmatic heuristics — NO LLM. The editor's model
168
+ * already reasons; chamba only checks the plan's structure for known anti-patterns.
169
+ */
170
+ declare function validatePlan(input: ValidatePlanInput): ValidationResult;
171
+
172
+ interface PlanReview {
173
+ approved: boolean;
174
+ issues: Issue[];
175
+ suggestions: string[];
176
+ riskFlags: string[];
177
+ }
178
+ interface ReviewInput {
179
+ plan: string;
180
+ task: string;
181
+ context?: string;
182
+ workspace?: Workspace;
183
+ }
184
+ /**
185
+ * Heuristic plan reviewer. Runs `validatePlan` and approves only when there are
186
+ * no error-severity issues. Warnings and risk flags are surfaced but don't block.
187
+ * NO LLM — the editor's model decides what to do with the review.
188
+ */
189
+ declare class Reviewer {
190
+ review(input: ReviewInput): PlanReview;
191
+ }
192
+
193
+ type WorkerKind = 'implementer' | 'tester' | 'reviewer';
194
+ interface SubtaskSpec {
195
+ title: string;
196
+ worker: WorkerKind;
197
+ description: string;
198
+ filesLikelyTouched: string[];
199
+ }
200
+ interface GeneratePlanInput {
201
+ task: string;
202
+ context?: string;
203
+ workspace?: Workspace;
204
+ }
205
+ /** Default subtask scaffold every plan starts from. The model refines these. */
206
+ declare function suggestSubtasks(): SubtaskSpec[];
207
+ /** Seed "files likely touched" from the workspace map (top-level dirs/projects). */
208
+ declare function suggestFilesLikelyTouched(workspace?: Workspace): string[];
209
+ /**
210
+ * Produce a structured plan *template* (not a finished plan). The editor's model
211
+ * fills the placeholders. chamba never writes the actual plan — it only provides
212
+ * the skeleton and, later, reviews it. No LLM involved.
213
+ */
214
+ declare function generatePlanTemplate(input: GeneratePlanInput): string;
215
+
216
+ interface ProcessResult {
217
+ stdout: string;
218
+ stderr: string;
219
+ exitCode: number;
220
+ }
221
+ interface ProcessExecOptions {
222
+ cwd?: string;
223
+ }
224
+ /**
225
+ * Run external processes behind a port so `@chamba/core` never imports
226
+ * `node:child_process`. Used from Phase 5 onward for git worktrees.
227
+ */
228
+ interface ProcessPort {
229
+ exec(command: string, args: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
230
+ }
231
+
232
+ interface RecordedCall {
233
+ command: string;
234
+ args: string[];
235
+ cwd?: string;
236
+ }
237
+ type ProcessHandler = (command: string, args: string[]) => Partial<ProcessResult>;
238
+ /**
239
+ * In-memory `ProcessPort` for tests. Records every call and returns whatever the
240
+ * handler produces (defaults to exit 0, empty output).
241
+ */
242
+ declare class FakeProcess implements ProcessPort {
243
+ private readonly handler;
244
+ readonly calls: RecordedCall[];
245
+ constructor(handler?: ProcessHandler);
246
+ exec(command: string, args: string[], options?: ProcessExecOptions): Promise<ProcessResult>;
247
+ }
248
+
249
+ /**
250
+ * In-memory `FilesystemPort` for tests (CLAUDE.md: tests de IO usan FilesystemPort
251
+ * en memoria). Paths are posix-style. Construct with a map of `path -> contents`.
252
+ */
253
+ declare class MemoryFilesystem implements FilesystemPort {
254
+ private readonly files;
255
+ private readonly dirs;
256
+ constructor(initial?: Record<string, string>);
257
+ private norm;
258
+ private set;
259
+ readFile(path: string): Promise<string>;
260
+ writeFile(path: string, content: string): Promise<void>;
261
+ exists(path: string): Promise<boolean>;
262
+ mkdir(path: string): Promise<void>;
263
+ remove(path: string): Promise<void>;
264
+ readDir(path: string): Promise<DirEntry[]>;
265
+ }
266
+
267
+ /**
268
+ * Minimal posix-style path helpers. `@chamba/core` avoids `node:path` so it can
269
+ * run in non-Node runtimes; all internal paths use `/` as separator.
270
+ */
271
+ declare function joinPath(...parts: string[]): string;
272
+ declare function basename(path: string): string;
273
+ declare function dirname(path: string): string;
274
+ /** Extension including the dot (e.g. `.ts`), or `''` if none. */
275
+ declare function extname(path: string): string;
276
+
277
+ interface RelevantNote {
278
+ path: string;
279
+ /** Number of keyword hits that made this note relevant. */
280
+ score: number;
281
+ /** First line that matched a keyword, trimmed. */
282
+ snippet: string;
283
+ }
284
+ interface ContextBuildInput {
285
+ workspace: Workspace;
286
+ task: string;
287
+ /** When set, search this Obsidian vault for notes relevant to the task. */
288
+ vaultPath?: string;
289
+ /** Soft cap on the produced context, in estimated tokens (~4 chars/token). */
290
+ maxTokens?: number;
291
+ }
292
+ interface BuiltContext {
293
+ context: string;
294
+ relevantNotes: string[];
295
+ }
296
+ /**
297
+ * Build the context block injected into the editor model's reasoning: a summary
298
+ * of the workspace plus, when a vault is present, the notes most relevant to the
299
+ * task (simple keyword search — semantic search is V2).
300
+ */
301
+ declare class ContextBuilder {
302
+ private readonly fs;
303
+ constructor(fs: FilesystemPort);
304
+ build(input: ContextBuildInput): Promise<BuiltContext>;
305
+ private workspaceSection;
306
+ private notesSection;
307
+ private searchNotes;
308
+ private collectMarkdown;
309
+ private tryRead;
310
+ }
311
+
312
+ /**
313
+ * Line-based diff (LCS) producing a readable unified-ish output:
314
+ * ` ` unchanged, `- ` removed, `+ ` added.
315
+ *
316
+ * Used by `workspace_reload` to show the model what a re-scan would change,
317
+ * without ever overwriting the user's hand-edited `workspace.md`.
318
+ */
319
+ declare function diffLines(oldText: string, newText: string): string;
320
+ /** True when the two texts are identical line-for-line. */
321
+ declare function textsEqual(oldText: string, newText: string): boolean;
322
+
323
+ interface VaultDetection {
324
+ found: boolean;
325
+ path?: string;
326
+ noteCount?: number;
327
+ }
328
+ interface DetectOptions {
329
+ /** Explicit vault path (e.g. from CHAMBA_OBSIDIAN_VAULT_PATH). Authoritative. */
330
+ explicitPath?: string;
331
+ /** Directories to probe for a `.obsidian/` marker when no explicit path. */
332
+ searchRoots?: string[];
333
+ }
334
+ /**
335
+ * Detect an Obsidian vault. An explicit path wins (the user set it on purpose);
336
+ * otherwise probe common roots for a `.obsidian/` directory. `@chamba/core` has
337
+ * no `os`/`fs`, so candidate roots are passed in by the caller.
338
+ */
339
+ declare class ObsidianDetector {
340
+ private readonly fs;
341
+ constructor(fs: FilesystemPort);
342
+ detect(opts: DetectOptions): Promise<VaultDetection>;
343
+ private countNotes;
344
+ }
345
+
346
+ declare class WorkspaceScanner {
347
+ private readonly fs;
348
+ constructor(fs: FilesystemPort);
349
+ scan(root: string): Promise<Workspace>;
350
+ private loadIgnoreRules;
351
+ private walk;
352
+ private detectLanguages;
353
+ private detectProjects;
354
+ private readProject;
355
+ private detectConventions;
356
+ private detectDescription;
357
+ private findReadme;
358
+ private tryRead;
359
+ }
360
+
361
+ interface BranchNameInput {
362
+ /** `YYYY-MM-DD`. */
363
+ date: string;
364
+ taskSlug: string;
365
+ workerId: string;
366
+ }
367
+ /**
368
+ * Sanitize a value into a single git-ref-safe path component: lowercase, no
369
+ * spaces, no characters git forbids in refs (`~^:?*[\` etc.), no leading/trailing
370
+ * dots or dashes.
371
+ */
372
+ declare function slugifyForGit(value: string): string;
373
+ /** Branch convention: `chamba/<date>-<task-slug>/<worker-id>`. */
374
+ declare function buildBranchName(input: BranchNameInput): string;
375
+ /** Worktree directory relative to the repo root. */
376
+ declare function worktreeRelativePath(taskSlug: string, workerId: string): string;
377
+
378
+ /**
379
+ * Detect whether a directory is inside a git work tree, via
380
+ * `git rev-parse --is-inside-work-tree`. Caches per root for the session.
381
+ */
382
+ declare class GitDetector {
383
+ private readonly process;
384
+ private readonly cache;
385
+ constructor(process: ProcessPort);
386
+ isGitRepo(root: string): Promise<boolean>;
387
+ }
388
+
389
+ declare class WorktreeError extends Error {
390
+ readonly name = "WorktreeError";
391
+ }
392
+ interface WorktreeHandle {
393
+ branch: string;
394
+ path: string;
395
+ taskSlug: string;
396
+ workerId: string;
397
+ }
398
+ interface CreateWorktreeInput {
399
+ root: string;
400
+ taskSlug: string;
401
+ workerId: string;
402
+ /** `YYYY-MM-DD` for the branch name. */
403
+ date: string;
404
+ baseBranch?: string;
405
+ }
406
+ interface ListedWorktree {
407
+ path: string;
408
+ head?: string;
409
+ branch?: string;
410
+ }
411
+ interface CleanupResult {
412
+ removed: boolean;
413
+ branchKept: boolean;
414
+ mergeSuggestion: string;
415
+ }
416
+ /**
417
+ * Manage git worktrees for isolated parallel work.
418
+ *
419
+ * Safety guarantees (see CLAUDE.md gotchas):
420
+ * - `cleanup` removes the worktree directory but NEVER deletes the branch and
421
+ * NEVER merges. The branch stays for the human to review and merge by hand.
422
+ * - `git worktree remove` is run WITHOUT `--force`, so a dirty worktree fails
423
+ * loudly instead of silently discarding work.
424
+ */
425
+ declare class WorktreeManager {
426
+ private readonly process;
427
+ constructor(process: ProcessPort);
428
+ create(input: CreateWorktreeInput): Promise<WorktreeHandle>;
429
+ list(root: string): Promise<ListedWorktree[]>;
430
+ cleanup(root: string, branch: string): Promise<CleanupResult>;
431
+ }
432
+
433
+ export { type BranchNameInput, type BuiltContext, type CleanupResult, type ClockPort, type ContextBuildInput, ContextBuilder, type CreateWorktreeInput, type DetectOptions, type DirEntry, FakeProcess, FilesystemMemoryStore, type FilesystemPort, type GeneratePlanInput, GitDetector, type Issue, type IssueSeverity, type ListedWorktree, MEMORY_DIR, type Memory, MemoryFilesystem, type MemoryStore, type NoteFields, ObsidianDetector, type PlanReview, type ProcessExecOptions, type ProcessHandler, type ProcessPort, type ProcessResult, type ProjectRef, type RecordedCall, type RelevantNote, type RememberInput, type ReviewInput, Reviewer, type SubtaskSpec, VAULT_NOTES_DIR, type ValidatePlanInput, type ValidationResult, type VaultDetection, VaultWriter, WORKSPACE_DIR, WORKSPACE_FILE, WORKSPACE_RELATIVE_PATH, type WorkerKind, type Workspace, WorkspaceScanner, WorktreeError, type WorktreeHandle, WorktreeManager, type WriteNoteInput, type WriteNoteResult, basename, buildBranchName, diffLines, dirname, extname, generatePlanTemplate, joinPath, renderNote, renderWorkspaceMarkdown, slugify, slugifyForGit, suggestFilesLikelyTouched, suggestSubtasks, textsEqual, validatePlan, worktreeRelativePath };