ithos 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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=ithos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ithos.d.ts","sourceRoot":"","sources":["../../src/bin/ithos.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { createCli } from "../cli.js";
3
+ await createCli().parseAsync(process.argv);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function createCli(): Command;
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,SAAS,IAAI,OAAO,CAoGnC"}
package/dist/cli.js ADDED
@@ -0,0 +1,100 @@
1
+ import { Command } from "commander";
2
+ import { doctorRepository, exportMemory, initRepository, isArtifactType, recordArtifact, searchMemory, validateRepository } from "ithos-core";
3
+ export function createCli() {
4
+ const program = new Command();
5
+ program
6
+ .name("ithos")
7
+ .description("Local-first engineering memory")
8
+ .version("0.1.0");
9
+ program
10
+ .command("init")
11
+ .description("Initialize Ithos memory in the current repository")
12
+ .action(async () => {
13
+ const result = await initRepository();
14
+ printList("Created", result.created);
15
+ printList("Already existed", result.existing);
16
+ });
17
+ program
18
+ .command("validate")
19
+ .description("Validate the Ithos repository structure")
20
+ .action(async () => {
21
+ const result = await validateRepository();
22
+ if (result.valid) {
23
+ console.log("Ithos repository is valid.");
24
+ return;
25
+ }
26
+ result.errors.forEach((error) => console.error(error));
27
+ process.exitCode = 1;
28
+ });
29
+ program
30
+ .command("doctor")
31
+ .description("Check local Ithos setup and print guidance")
32
+ .action(async () => {
33
+ const result = await doctorRepository();
34
+ result.messages.forEach((message) => console.log(message));
35
+ if (!result.ok) {
36
+ process.exitCode = 1;
37
+ }
38
+ });
39
+ program
40
+ .command("record")
41
+ .description("Record one engineering memory artifact")
42
+ .requiredOption("-t, --type <type>", "artifact type")
43
+ .requiredOption("--title <title>", "artifact title")
44
+ .option("-b, --body <body>", "artifact body")
45
+ .action(async (options) => {
46
+ if (!isArtifactType(options.type)) {
47
+ console.error(`Unsupported artifact type: ${options.type}`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ const body = options.body ?? (await readStdin());
52
+ if (body.trim().length === 0) {
53
+ console.error("Artifact body is required through --body or stdin.");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ const file = await recordArtifact({
58
+ type: options.type,
59
+ title: options.title,
60
+ body
61
+ });
62
+ console.log(`Recorded ${file}`);
63
+ });
64
+ program
65
+ .command("search")
66
+ .description("Search Ithos markdown memory")
67
+ .argument("<query>", "keyword to search for")
68
+ .action(async (query) => {
69
+ const results = await searchMemory(query);
70
+ if (results.length === 0) {
71
+ console.log("No matches found.");
72
+ return;
73
+ }
74
+ results.forEach((result) => {
75
+ console.log(`${result.file}:${result.line}: ${result.text}`);
76
+ });
77
+ });
78
+ program
79
+ .command("export")
80
+ .description("Export Ithos markdown memory to stdout")
81
+ .action(async () => {
82
+ const result = await exportMemory();
83
+ process.stdout.write(result.markdown);
84
+ });
85
+ return program;
86
+ }
87
+ function printList(label, values) {
88
+ if (values.length === 0) {
89
+ return;
90
+ }
91
+ console.log(`${label}:`);
92
+ values.forEach((value) => console.log(`- ${value}`));
93
+ }
94
+ async function readStdin() {
95
+ const chunks = [];
96
+ for await (const chunk of process.stdin) {
97
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
98
+ }
99
+ return Buffer.concat(chunks).toString("utf8");
100
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createCli } from "./cli.js";
4
+ test("createCli returns a configured commander program", () => {
5
+ const program = createCli();
6
+ assert.equal(program.name(), "ithos");
7
+ const commands = program.commands.map((cmd) => cmd.name());
8
+ assert.ok(commands.includes("init"));
9
+ assert.ok(commands.includes("validate"));
10
+ assert.ok(commands.includes("doctor"));
11
+ assert.ok(commands.includes("record"));
12
+ assert.ok(commands.includes("search"));
13
+ assert.ok(commands.includes("export"));
14
+ });
@@ -0,0 +1,2 @@
1
+ export { createCli } from "./cli.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createCli } from "./cli.js";
@@ -0,0 +1,37 @@
1
+ export type ArtifactType = "architecture" | "decisions" | "defects" | "features" | "gaps" | "lessons" | "patterns" | "regressions" | "releases" | "sessions";
2
+ export type InitResult = {
3
+ created: string[];
4
+ existing: string[];
5
+ };
6
+ export type ValidationResult = {
7
+ valid: boolean;
8
+ errors: string[];
9
+ };
10
+ export type DoctorResult = {
11
+ ok: boolean;
12
+ messages: string[];
13
+ };
14
+ export type RecordInput = {
15
+ type: ArtifactType;
16
+ title: string;
17
+ body: string;
18
+ cwd?: string;
19
+ date?: Date;
20
+ };
21
+ export type SearchResult = {
22
+ file: string;
23
+ line: number;
24
+ text: string;
25
+ };
26
+ export type ExportResult = {
27
+ files: string[];
28
+ markdown: string;
29
+ };
30
+ export declare function isArtifactType(value: string): value is ArtifactType;
31
+ export declare function initRepository(cwd?: string): Promise<InitResult>;
32
+ export declare function validateRepository(cwd?: string): Promise<ValidationResult>;
33
+ export declare function doctorRepository(cwd?: string): Promise<DoctorResult>;
34
+ export declare function recordArtifact(input: RecordInput): Promise<string>;
35
+ export declare function searchMemory(query: string, cwd?: string): Promise<SearchResult[]>;
36
+ export declare function exportMemory(cwd?: string): Promise<ExportResult>;
37
+ //# sourceMappingURL=ithos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ithos.d.ts","sourceRoot":"","sources":["../src/ithos.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,WAAW,GACX,SAAS,GACT,UAAU,GACV,MAAM,GACN,SAAS,GACT,UAAU,GACV,aAAa,GACb,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAgCF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,YAAY,CAEnE;AAED,wBAAsB,cAAc,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoB7E;AAED,wBAAsB,kBAAkB,CACtC,GAAG,SAAgB,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAa3B;AAED,wBAAsB,gBAAgB,CACpC,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,CAAC,CAsBvB;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCxE;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CA0BzB;AAED,wBAAsB,YAAY,CAAC,GAAG,SAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmB7E"}
package/dist/ithos.js ADDED
@@ -0,0 +1,278 @@
1
+ import { readdir, readFile, stat, writeFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const REQUIRED_FILES = [".ithos/README.md", ".ithos/project.md"];
4
+ const ARTIFACT_TYPES = [
5
+ "architecture",
6
+ "decisions",
7
+ "defects",
8
+ "features",
9
+ "gaps",
10
+ "lessons",
11
+ "patterns",
12
+ "regressions",
13
+ "releases",
14
+ "sessions"
15
+ ];
16
+ const DIRECTORY_LAYOUT = [
17
+ ".ithos",
18
+ ".ithos/sessions",
19
+ ".ithos/decisions",
20
+ ".ithos/architecture",
21
+ ".ithos/features",
22
+ ".ithos/patterns",
23
+ ".ithos/defects",
24
+ ".ithos/regressions",
25
+ ".ithos/gaps",
26
+ ".ithos/lessons",
27
+ ".ithos/releases",
28
+ ".ithos/attachments"
29
+ ];
30
+ export function isArtifactType(value) {
31
+ return ARTIFACT_TYPES.includes(value);
32
+ }
33
+ export async function initRepository(cwd = process.cwd()) {
34
+ const created = [];
35
+ const existing = [];
36
+ for (const directory of DIRECTORY_LAYOUT) {
37
+ const result = await ensureDirectory(path.join(cwd, directory));
38
+ pushResult(result, directory, created, existing);
39
+ }
40
+ const files = new Map([
41
+ [".ithos/README.md", ithosReadme()],
42
+ [".ithos/project.md", projectTemplate()]
43
+ ]);
44
+ for (const [file, content] of files) {
45
+ const result = await ensureFile(path.join(cwd, file), content);
46
+ pushResult(result, file, created, existing);
47
+ }
48
+ return { created, existing };
49
+ }
50
+ export async function validateRepository(cwd = process.cwd()) {
51
+ const errors = [];
52
+ for (const file of REQUIRED_FILES) {
53
+ if (!(await exists(path.join(cwd, file)))) {
54
+ errors.push(`Missing required file: ${file}`);
55
+ }
56
+ }
57
+ return {
58
+ valid: errors.length === 0,
59
+ errors
60
+ };
61
+ }
62
+ export async function doctorRepository(cwd = process.cwd()) {
63
+ const messages = [];
64
+ const validation = await validateRepository(cwd);
65
+ if (validation.valid) {
66
+ messages.push("Ithos repository structure is valid.");
67
+ }
68
+ else {
69
+ messages.push("Ithos repository structure needs attention.");
70
+ messages.push(...validation.errors);
71
+ messages.push("Run `ithos init` to create the required files.");
72
+ }
73
+ if (await exists(path.join(cwd, ".git"))) {
74
+ messages.push("Git repository detected.");
75
+ }
76
+ else {
77
+ messages.push("No .git directory detected. Ithos works best inside Git.");
78
+ }
79
+ return {
80
+ ok: validation.valid,
81
+ messages
82
+ };
83
+ }
84
+ export async function recordArtifact(input) {
85
+ const cwd = input.cwd ?? process.cwd();
86
+ const now = input.date ?? new Date();
87
+ const created = formatDate(now);
88
+ const id = createArtifactId(input.type, input.title, now);
89
+ const relativeFile = path.join(".ithos", input.type, `${slugify(input.title)}.md`);
90
+ const absoluteFile = path.join(cwd, relativeFile);
91
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
92
+ const markdown = [
93
+ "---",
94
+ `id: ${id}`,
95
+ `type: ${input.type}`,
96
+ "status: draft",
97
+ `created: ${created}`,
98
+ `updated: ${created}`,
99
+ "tags: []",
100
+ "---",
101
+ "",
102
+ `# ${input.title}`,
103
+ "",
104
+ input.body.trim(),
105
+ ""
106
+ ].join("\n");
107
+ await writeFile(absoluteFile, markdown, { flag: "wx" });
108
+ return relativeFile;
109
+ }
110
+ export async function searchMemory(query, cwd = process.cwd()) {
111
+ const root = path.join(cwd, ".ithos");
112
+ if (!(await exists(root))) {
113
+ return [];
114
+ }
115
+ const files = await listMarkdownFiles(root);
116
+ const normalizedQuery = query.toLowerCase();
117
+ const results = [];
118
+ for (const file of files) {
119
+ const content = await readFile(file, "utf8");
120
+ const lines = searchableLines(content);
121
+ lines.forEach((line) => {
122
+ if (line.text.toLowerCase().includes(normalizedQuery)) {
123
+ results.push({
124
+ file: path.relative(cwd, file),
125
+ line: line.number,
126
+ text: line.text.trim()
127
+ });
128
+ }
129
+ });
130
+ }
131
+ return results;
132
+ }
133
+ export async function exportMemory(cwd = process.cwd()) {
134
+ const root = path.join(cwd, ".ithos");
135
+ if (!(await exists(root))) {
136
+ return { files: [], markdown: "" };
137
+ }
138
+ const files = await listMarkdownFiles(root);
139
+ const sections = [];
140
+ for (const file of files) {
141
+ const relativeFile = path.relative(cwd, file);
142
+ const content = await readFile(file, "utf8");
143
+ sections.push([`<!-- ${relativeFile} -->`, content.trim(), ""].join("\n"));
144
+ }
145
+ return {
146
+ files: files.map((file) => path.relative(cwd, file)),
147
+ markdown: sections.join("\n")
148
+ };
149
+ }
150
+ async function ensureDirectory(directory) {
151
+ if (await exists(directory)) {
152
+ return "existing";
153
+ }
154
+ await mkdir(directory, { recursive: true });
155
+ return "created";
156
+ }
157
+ async function ensureFile(file, content) {
158
+ if (await exists(file)) {
159
+ return "existing";
160
+ }
161
+ await writeFile(file, content, { flag: "wx" });
162
+ return "created";
163
+ }
164
+ function pushResult(result, file, created, existing) {
165
+ if (result === "created") {
166
+ created.push(file);
167
+ }
168
+ else {
169
+ existing.push(file);
170
+ }
171
+ }
172
+ async function exists(file) {
173
+ try {
174
+ await stat(file);
175
+ return true;
176
+ }
177
+ catch (error) {
178
+ if (isNodeError(error) && error.code === "ENOENT") {
179
+ return false;
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+ async function listMarkdownFiles(directory) {
185
+ const entries = await readdir(directory, { withFileTypes: true });
186
+ const files = await Promise.all(entries.map(async (entry) => {
187
+ const absolutePath = path.join(directory, entry.name);
188
+ if (entry.isDirectory()) {
189
+ return listMarkdownFiles(absolutePath);
190
+ }
191
+ if (entry.isFile() && entry.name.endsWith(".md")) {
192
+ return [absolutePath];
193
+ }
194
+ return [];
195
+ }));
196
+ return files.flat().sort();
197
+ }
198
+ function isNodeError(error) {
199
+ return error instanceof Error && "code" in error;
200
+ }
201
+ function formatDate(date) {
202
+ const year = date.getFullYear();
203
+ const month = String(date.getMonth() + 1).padStart(2, "0");
204
+ const day = String(date.getDate()).padStart(2, "0");
205
+ return `${year}-${month}-${day}`;
206
+ }
207
+ function createArtifactId(type, title, date) {
208
+ const prefix = type.slice(0, 3).toUpperCase();
209
+ const timestamp = [
210
+ date.getFullYear(),
211
+ String(date.getMonth() + 1).padStart(2, "0"),
212
+ String(date.getDate()).padStart(2, "0"),
213
+ String(date.getHours()).padStart(2, "0"),
214
+ String(date.getMinutes()).padStart(2, "0"),
215
+ String(date.getSeconds()).padStart(2, "0")
216
+ ].join("");
217
+ return `${prefix}-${timestamp}-${slugify(title)}`;
218
+ }
219
+ function searchableLines(content) {
220
+ const lines = content.split("\n");
221
+ if (!content.startsWith("---\n")) {
222
+ return lines.map((text, index) => ({ number: index + 1, text }));
223
+ }
224
+ const end = content.indexOf("\n---\n", 4);
225
+ if (end === -1) {
226
+ return lines.map((text, index) => ({ number: index + 1, text }));
227
+ }
228
+ const frontmatterLineCount = content
229
+ .slice(0, end + "\n---\n".length)
230
+ .split("\n").length;
231
+ return lines
232
+ .map((text, index) => ({ number: index + 1, text }))
233
+ .filter((line) => line.number > frontmatterLineCount);
234
+ }
235
+ function slugify(value) {
236
+ const slug = value
237
+ .trim()
238
+ .toLowerCase()
239
+ .replace(/[^a-z0-9]+/g, "-")
240
+ .replace(/^-+|-+$/g, "");
241
+ return slug || "untitled";
242
+ }
243
+ function ithosReadme() {
244
+ return `# Ithos
245
+
246
+ This directory preserves the engineering memory for this repository.
247
+
248
+ Ithos records why the software changed: decisions, patterns, lessons, defects,
249
+ regressions, gaps, sessions and releases.
250
+
251
+ Capture signal, not exhaust. Markdown is the source of truth.
252
+ `;
253
+ }
254
+ function projectTemplate() {
255
+ return `# Project
256
+
257
+ ## Summary
258
+
259
+ Describe what this project is and why it exists.
260
+
261
+ ## Goals
262
+
263
+ - Preserve engineering memory locally.
264
+
265
+ ## Technology Stack
266
+
267
+ - TypeScript
268
+
269
+ ## Architecture
270
+
271
+ Describe the high-level architecture as it evolves.
272
+
273
+ ## Conventions
274
+
275
+ - Keep markdown human-readable.
276
+ - Prefer concise, factual engineering memory.
277
+ `;
278
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ithos.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ithos.test.d.ts","sourceRoot":"","sources":["../src/ithos.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,131 @@
1
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import test from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { exportMemory, initRepository, recordArtifact, searchMemory, validateRepository } from "./ithos.js";
7
+ test("initRepository creates the required Ithos files", async () => {
8
+ const cwd = await temporaryDirectory();
9
+ try {
10
+ const result = await initRepository(cwd);
11
+ assert.ok(result.created.includes(".ithos/README.md"));
12
+ assert.ok(result.created.includes(".ithos/project.md"));
13
+ const validation = await validateRepository(cwd);
14
+ assert.equal(validation.valid, true);
15
+ assert.deepEqual(validation.errors, []);
16
+ }
17
+ finally {
18
+ await removeDirectory(cwd);
19
+ }
20
+ });
21
+ test("validateRepository reports missing required files", async () => {
22
+ const cwd = await temporaryDirectory();
23
+ try {
24
+ const validation = await validateRepository(cwd);
25
+ assert.equal(validation.valid, false);
26
+ assert.deepEqual(validation.errors, [
27
+ "Missing required file: .ithos/README.md",
28
+ "Missing required file: .ithos/project.md"
29
+ ]);
30
+ }
31
+ finally {
32
+ await removeDirectory(cwd);
33
+ }
34
+ });
35
+ test("recordArtifact creates a markdown artifact without overwriting", async () => {
36
+ const cwd = await temporaryDirectory();
37
+ try {
38
+ await initRepository(cwd);
39
+ const file = await recordArtifact({
40
+ cwd,
41
+ type: "decisions",
42
+ title: "Use Markdown",
43
+ body: "Markdown stays readable in Git.",
44
+ date: new Date("2026-07-02T12:00:00.000Z")
45
+ });
46
+ assert.equal(file, ".ithos/decisions/use-markdown.md");
47
+ const content = await readFile(path.join(cwd, file), "utf8");
48
+ assert.match(content, /type: decisions/);
49
+ assert.match(content, /# Use Markdown/);
50
+ assert.match(content, /Markdown stays readable in Git\./);
51
+ await assert.rejects(() => recordArtifact({
52
+ cwd,
53
+ type: "decisions",
54
+ title: "Use Markdown",
55
+ body: "This should not overwrite the original.",
56
+ date: new Date("2026-07-02T12:01:00.000Z")
57
+ }), { code: "EEXIST" });
58
+ }
59
+ finally {
60
+ await removeDirectory(cwd);
61
+ }
62
+ });
63
+ test("recordArtifact uses local dates in frontmatter and ids", async () => {
64
+ const cwd = await temporaryDirectory();
65
+ try {
66
+ await initRepository(cwd);
67
+ const file = await recordArtifact({
68
+ cwd,
69
+ type: "sessions",
70
+ title: "Local Date",
71
+ body: "Dates should reflect the local developer calendar.",
72
+ date: new Date(2026, 6, 3, 0, 5, 0)
73
+ });
74
+ const content = await readFile(path.join(cwd, file), "utf8");
75
+ assert.match(content, /id: SES-20260703000500-local-date/);
76
+ assert.match(content, /created: 2026-07-03/);
77
+ assert.match(content, /updated: 2026-07-03/);
78
+ }
79
+ finally {
80
+ await removeDirectory(cwd);
81
+ }
82
+ });
83
+ test("searchMemory returns matching markdown lines", async () => {
84
+ const cwd = await temporaryDirectory();
85
+ try {
86
+ await initRepository(cwd);
87
+ await recordArtifact({
88
+ cwd,
89
+ type: "lessons",
90
+ title: "Capture Signal",
91
+ body: "Capture signal, not exhaust.",
92
+ date: new Date("2026-07-02T12:00:00.000Z")
93
+ });
94
+ const results = await searchMemory("not exhaust", cwd);
95
+ assert.deepEqual(results, [
96
+ {
97
+ file: ".ithos/README.md",
98
+ line: 8,
99
+ text: "Capture signal, not exhaust. Markdown is the source of truth."
100
+ },
101
+ {
102
+ file: ".ithos/lessons/capture-signal.md",
103
+ line: 12,
104
+ text: "Capture signal, not exhaust."
105
+ }
106
+ ]);
107
+ }
108
+ finally {
109
+ await removeDirectory(cwd);
110
+ }
111
+ });
112
+ test("exportMemory concatenates markdown files", async () => {
113
+ const cwd = await temporaryDirectory();
114
+ try {
115
+ await initRepository(cwd);
116
+ const result = await exportMemory(cwd);
117
+ assert.ok(result.files.includes(".ithos/README.md"));
118
+ assert.ok(result.files.includes(".ithos/project.md"));
119
+ assert.match(result.markdown, /<!-- \.ithos\/README\.md -->/);
120
+ assert.match(result.markdown, /<!-- \.ithos\/project\.md -->/);
121
+ }
122
+ finally {
123
+ await removeDirectory(cwd);
124
+ }
125
+ });
126
+ async function temporaryDirectory() {
127
+ return mkdtemp(path.join(os.tmpdir(), "ithos-test-"));
128
+ }
129
+ async function removeDirectory(directory) {
130
+ await rm(directory, { recursive: true, force: true });
131
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "ithos",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "bin": {
10
+ "ithos": "./dist/bin/ithos.js"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json",
25
+ "test": "npm run build && node --test dist/*.test.js",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "ithos-core": "*",
30
+ "commander": "^14.0.0"
31
+ }
32
+ }