@pedrocivita/tocket 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +193 -132
  2. package/dist/commands/dashboard.js +5 -0
  3. package/dist/commands/doctor.cmd.d.ts +9 -0
  4. package/dist/commands/doctor.cmd.js +191 -0
  5. package/dist/commands/eject.cmd.d.ts +6 -0
  6. package/dist/commands/eject.cmd.js +65 -0
  7. package/dist/commands/focus.cmd.d.ts +7 -0
  8. package/dist/commands/focus.cmd.js +52 -0
  9. package/dist/commands/generate.cmd.d.ts +8 -0
  10. package/dist/commands/generate.cmd.js +27 -10
  11. package/dist/commands/init.cmd.js +15 -3
  12. package/dist/commands/lint.cmd.d.ts +12 -0
  13. package/dist/commands/lint.cmd.js +269 -0
  14. package/dist/commands/status.cmd.d.ts +2 -0
  15. package/dist/commands/status.cmd.js +85 -0
  16. package/dist/commands/sync.cmd.js +3 -2
  17. package/dist/commands/validate.cmd.d.ts +7 -0
  18. package/dist/commands/validate.cmd.js +3 -3
  19. package/dist/index.js +10 -0
  20. package/dist/tests/doctor.test.d.ts +1 -0
  21. package/dist/tests/doctor.test.js +102 -0
  22. package/dist/tests/eject.test.d.ts +1 -0
  23. package/dist/tests/eject.test.js +88 -0
  24. package/dist/tests/focus.test.d.ts +1 -0
  25. package/dist/tests/focus.test.js +130 -0
  26. package/dist/tests/generate.test.d.ts +1 -0
  27. package/dist/tests/generate.test.js +67 -0
  28. package/dist/tests/git.test.js +9 -1
  29. package/dist/tests/init.test.d.ts +1 -0
  30. package/dist/tests/init.test.js +69 -0
  31. package/dist/tests/lint.test.d.ts +1 -0
  32. package/dist/tests/lint.test.js +120 -0
  33. package/dist/tests/status.test.d.ts +1 -0
  34. package/dist/tests/status.test.js +34 -0
  35. package/dist/tests/validate.test.d.ts +1 -0
  36. package/dist/tests/validate.test.js +92 -0
  37. package/dist/utils/git.d.ts +1 -0
  38. package/dist/utils/git.js +13 -0
  39. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { success, error as themeError, info } from "../utils/theme.js";
6
+ /**
7
+ * Replaces the content under ## Current Focus with the new message.
8
+ * Exported for testing.
9
+ */
10
+ export function replaceFocusSection(content, newFocus) {
11
+ const regex = /(## Current Focus[ \t]*\n)[\s\S]*?(?=\n## |\n*$)/;
12
+ if (!regex.test(content)) {
13
+ return content.trimEnd() + "\n\n## Current Focus\n\n" + newFocus + "\n";
14
+ }
15
+ return content.replace(regex, `$1\n${newFocus}\n`);
16
+ }
17
+ export function registerFocusCommand(program) {
18
+ program
19
+ .command("focus")
20
+ .description("Update the Current Focus in activeContext.md")
21
+ .argument("[message...]", "Focus message (prompted if omitted)")
22
+ .action(async (messageParts) => {
23
+ const cwd = process.cwd();
24
+ const contextDir = join(cwd, ".context");
25
+ if (!existsSync(contextDir)) {
26
+ console.error(themeError("No .context/ directory found. Run 'tocket init' first."));
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ const filePath = join(contextDir, "activeContext.md");
31
+ if (!existsSync(filePath)) {
32
+ console.error(themeError("activeContext.md not found. Run 'tocket init' first."));
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ let message = messageParts.join(" ").trim();
37
+ if (!message) {
38
+ message = await input({ message: "What is the current focus?" });
39
+ message = message.trim();
40
+ }
41
+ if (!message) {
42
+ console.error(themeError("Focus message cannot be empty."));
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const content = await readFile(filePath, "utf-8");
47
+ const updated = replaceFocusSection(content, message);
48
+ await writeFile(filePath, updated, "utf-8");
49
+ console.log(info("Focus updated in .context/activeContext.md"));
50
+ console.log(" " + success(message));
51
+ });
52
+ }
@@ -1,2 +1,10 @@
1
1
  import type { Command } from "commander";
2
+ export interface TaskInput {
3
+ intent: string;
4
+ scope: string;
5
+ priority: string;
6
+ skills: string;
7
+ }
8
+ export declare function buildPayloadXml(tasks: TaskInput[]): string;
9
+ export declare function suggestScope(): string;
2
10
  export declare function registerGenerateCommand(program: Command): void;
@@ -1,9 +1,9 @@
1
1
  import { input, select, confirm } from "@inquirer/prompts";
2
- import clipboard from "clipboardy";
2
+ import { writeFileSync } from "node:fs";
3
3
  import { success, heading, dim, banner } from "../utils/theme.js";
4
4
  import { getConfig } from "../utils/config.js";
5
- import { getStagedFiles, getModifiedFiles } from "../utils/git.js";
6
- function buildPayloadXml(tasks) {
5
+ import { getStagedFiles, getModifiedFiles, getLastCommitMessage } from "../utils/git.js";
6
+ export function buildPayloadXml(tasks) {
7
7
  const first = tasks[0];
8
8
  const skillsAttr = first.skills.trim()
9
9
  ? `\n <skills>${first.skills.trim()}</skills>`
@@ -36,7 +36,7 @@ ${taskBlocks}
36
36
  </validate>
37
37
  </payload>`;
38
38
  }
39
- function suggestScope() {
39
+ export function suggestScope() {
40
40
  const staged = getStagedFiles();
41
41
  const modified = getModifiedFiles();
42
42
  const all = [...new Set([...staged, ...modified])];
@@ -47,6 +47,7 @@ export function registerGenerateCommand(program) {
47
47
  .command("generate")
48
48
  .description("Build payload XMLs interactively for Architect-Executor handoff")
49
49
  .option("--no-preview", "Skip payload preview before copying")
50
+ .option("--to <target>", "Output target: clipboard (default), stdout, or file path")
50
51
  .action(async (options) => {
51
52
  const config = await getConfig();
52
53
  if (!config.theme?.disableBanner) {
@@ -62,7 +63,11 @@ export function registerGenerateCommand(program) {
62
63
  if (taskNum > 1) {
63
64
  console.log(heading(`\n Task ${taskNum}\n`));
64
65
  }
65
- const intent = await input({ message: "Intent (goal in one line):" });
66
+ const lastCommit = getLastCommitMessage();
67
+ const intent = await input({
68
+ message: "Intent (goal in one line):",
69
+ default: lastCommit || undefined,
70
+ });
66
71
  const scope = await input({
67
72
  message: "Scope (files/folders affected):",
68
73
  default: taskNum === 1 && suggestedScope ? suggestedScope : undefined,
@@ -97,10 +102,22 @@ export function registerGenerateCommand(program) {
97
102
  console.log(dim(preview));
98
103
  console.log(dim("--- End Preview ---\n"));
99
104
  }
100
- clipboard.writeSync(xml);
101
- console.log(success(`Payload XML (v2.0) copied to clipboard!`) +
102
- dim(` ${tasks.length} task(s).`) +
103
- "\n" +
104
- dim(" Paste it into your Architect to continue.\n"));
105
+ const target = options.to ?? "clipboard";
106
+ if (target === "stdout") {
107
+ console.log(xml);
108
+ }
109
+ else if (target === "clipboard") {
110
+ const { default: clipboard } = await import("clipboardy");
111
+ clipboard.writeSync(xml);
112
+ console.log(success(`Payload XML (v2.0) copied to clipboard!`) +
113
+ dim(` ${tasks.length} task(s).`) +
114
+ "\n" +
115
+ dim(" Paste it into your Architect to continue.\n"));
116
+ }
117
+ else {
118
+ writeFileSync(target, xml, "utf-8");
119
+ console.log(success(`Payload XML (v2.0) written to ${target}!`) +
120
+ dim(` ${tasks.length} task(s).\n`));
121
+ }
105
122
  });
106
123
  }
@@ -119,8 +119,12 @@ export function registerInitCommand(program) {
119
119
  .command("init")
120
120
  .description("Scaffold an agentic workspace with Memory Bank and triangulation config")
121
121
  .option("-f, --force", "Overwrite existing files without prompting")
122
+ .option("--minimal", "Scaffold only essential files (.context/ + TOCKET.md)")
123
+ .option("--name <name>", "Project name (skip prompt)")
124
+ .option("--description <desc>", "Project description (skip prompt)")
122
125
  .action(async (options) => {
123
126
  const force = options.force ?? false;
127
+ const minimal = options.minimal ?? false;
124
128
  const cwd = process.cwd();
125
129
  const globalConfig = await getConfig();
126
130
  if (!globalConfig.theme?.disableBanner) {
@@ -142,11 +146,11 @@ export function registerInitCommand(program) {
142
146
  console.log(info(`Extras: ${stack.extras.join(", ")}`));
143
147
  console.log();
144
148
  }
145
- const projectName = await input({
149
+ const projectName = options.name ?? await input({
146
150
  message: "Project Name:",
147
151
  default: detectedName || undefined,
148
152
  });
149
- const description = await input({
153
+ const description = options.description ?? await input({
150
154
  message: "Short Description:",
151
155
  default: detectedDescription || undefined,
152
156
  });
@@ -169,7 +173,15 @@ export function registerInitCommand(program) {
169
173
  ],
170
174
  [join(".context", "progress.md"), progressMd(projectName)],
171
175
  ];
172
- for (const [filePath, content] of files) {
176
+ const minimalPaths = new Set([
177
+ "TOCKET.md",
178
+ join(".context", "activeContext.md"),
179
+ join(".context", "systemPatterns.md"),
180
+ ]);
181
+ const filesToWrite = minimal
182
+ ? files.filter(([path]) => minimalPaths.has(path))
183
+ : files;
184
+ for (const [filePath, content] of filesToWrite) {
173
185
  const fullPath = join(cwd, filePath);
174
186
  const exists = await fileExists(fullPath);
175
187
  if (exists && !force) {
@@ -0,0 +1,12 @@
1
+ import type { Command } from "commander";
2
+ export interface LintResult {
3
+ severity: "pass" | "warn" | "info" | "fail";
4
+ message: string;
5
+ file?: string;
6
+ suggestion?: string;
7
+ }
8
+ export declare function lintActiveContext(content: string): LintResult[];
9
+ export declare function lintSystemPatterns(content: string): LintResult[];
10
+ export declare function lintProtocolSpec(content: string): LintResult[];
11
+ export declare function lintAgentConfig(name: string, content: string): LintResult[];
12
+ export declare function registerLintCommand(program: Command): void;
@@ -0,0 +1,269 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { success as themePass, warn as themeWarn, error as themeFail, heading, dim, info as themeInfo } from "../utils/theme.js";
5
+ import { isGitRepo } from "../utils/git.js";
6
+ export function lintActiveContext(content) {
7
+ const results = [];
8
+ // Check required sections exist
9
+ const requiredSections = ["## Current Focus", "## Recent Changes", "## Open Decisions"];
10
+ for (const section of requiredSections) {
11
+ if (content.includes(section)) {
12
+ results.push({ severity: "pass", message: `Has ${section} section` });
13
+ }
14
+ else {
15
+ results.push({
16
+ severity: "fail",
17
+ message: `Missing ${section} section`,
18
+ suggestion: `Add a "${section}" heading to activeContext.md`,
19
+ });
20
+ }
21
+ }
22
+ // Check Current Focus is populated
23
+ const focusMatch = content.match(/## Current Focus\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
24
+ const focusBody = focusMatch?.[1]?.trim() ?? "";
25
+ if (!focusBody || focusBody.startsWith("_") || focusBody.includes("No active tasks")) {
26
+ results.push({
27
+ severity: "warn",
28
+ message: "Current Focus is empty or placeholder",
29
+ suggestion: "Run tocket focus \"your current task\" to set it",
30
+ });
31
+ }
32
+ else {
33
+ results.push({ severity: "pass", message: "Current Focus is populated" });
34
+ }
35
+ // Check Open Decisions section
36
+ const decisionsMatch = content.match(/## Open Decisions\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
37
+ const decisionsBody = decisionsMatch?.[1]?.trim() ?? "";
38
+ if (!decisionsBody || decisionsBody.startsWith("_")) {
39
+ results.push({
40
+ severity: "info",
41
+ message: "Open Decisions section is empty",
42
+ suggestion: "List any unresolved decisions or write \"None\" if all resolved",
43
+ });
44
+ }
45
+ return results;
46
+ }
47
+ export function lintSystemPatterns(content) {
48
+ const results = [];
49
+ const hasConvention = content.includes("- ") || content.includes("| ");
50
+ if (hasConvention) {
51
+ results.push({ severity: "pass", message: "Has documented conventions" });
52
+ }
53
+ else {
54
+ results.push({
55
+ severity: "warn",
56
+ message: "No conventions documented",
57
+ suggestion: "Add bullet points or a table with your project's coding conventions",
58
+ });
59
+ }
60
+ // Check for key decisions table
61
+ if (content.includes("Key Decisions") || content.includes("key decisions")) {
62
+ const hasDecisionRows = content.match(/\|[^|]+\|[^|]+\|[^|]+\|/g);
63
+ const hasDataRows = hasDecisionRows && hasDecisionRows.length > 2; // header + separator + at least 1 row
64
+ if (hasDataRows) {
65
+ results.push({ severity: "pass", message: "Has key decisions recorded" });
66
+ }
67
+ else {
68
+ results.push({
69
+ severity: "info",
70
+ message: "Key Decisions table is empty",
71
+ suggestion: "Record architectural decisions with rationale and date",
72
+ });
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+ export function lintProtocolSpec(content) {
78
+ const results = [];
79
+ if (content.includes("payload") || content.includes("Payload")) {
80
+ results.push({ severity: "pass", message: "References payload format" });
81
+ }
82
+ else {
83
+ results.push({
84
+ severity: "warn",
85
+ message: "Does not reference payload format",
86
+ suggestion: "TOCKET.md should describe the payload XML handoff format",
87
+ });
88
+ }
89
+ if (content.includes("Memory Bank") || content.includes(".context/")) {
90
+ results.push({ severity: "pass", message: "References Memory Bank" });
91
+ }
92
+ else {
93
+ results.push({
94
+ severity: "warn",
95
+ message: "Does not reference Memory Bank",
96
+ suggestion: "TOCKET.md should describe the .context/ directory",
97
+ });
98
+ }
99
+ return results;
100
+ }
101
+ export function lintAgentConfig(name, content) {
102
+ const results = [];
103
+ if (content.includes(".context/") || content.includes(".context\\")) {
104
+ results.push({ severity: "pass", message: `References .context/` });
105
+ }
106
+ else {
107
+ results.push({
108
+ severity: "warn",
109
+ message: `Does not reference .context/`,
110
+ suggestion: `Add instructions to read .context/ before acting`,
111
+ });
112
+ }
113
+ return results;
114
+ }
115
+ function severityIcon(severity) {
116
+ switch (severity) {
117
+ case "pass": return themePass("");
118
+ case "warn": return themeWarn("");
119
+ case "info": return themeInfo("");
120
+ case "fail": return themeFail("");
121
+ }
122
+ }
123
+ export function registerLintCommand(program) {
124
+ program
125
+ .command("lint")
126
+ .description("Audit .context/ content quality and suggest improvements")
127
+ .action(() => {
128
+ const cwd = process.cwd();
129
+ let passCount = 0;
130
+ let warnCount = 0;
131
+ let infoCount = 0;
132
+ let failCount = 0;
133
+ console.log(heading("\nTocket Lint\n"));
134
+ // Check TOCKET.md exists
135
+ const tocketPath = join(cwd, "TOCKET.md");
136
+ if (!existsSync(tocketPath)) {
137
+ console.log(themeFail(" TOCKET.md not found. Run tocket init first."));
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ // Lint activeContext.md
142
+ const acPath = join(cwd, ".context", "activeContext.md");
143
+ if (existsSync(acPath)) {
144
+ console.log(dim(" .context/activeContext.md"));
145
+ const content = readFileSync(acPath, "utf-8");
146
+ const results = lintActiveContext(content);
147
+ for (const r of results) {
148
+ printResult(r);
149
+ countResult(r.severity);
150
+ }
151
+ // Staleness check
152
+ const stats = statSync(acPath);
153
+ const daysSince = Math.floor((Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24));
154
+ if (daysSince > 7) {
155
+ const r = {
156
+ severity: "warn",
157
+ message: `Last modified ${daysSince} days ago`,
158
+ suggestion: "Update activeContext.md with current state",
159
+ };
160
+ printResult(r);
161
+ countResult(r.severity);
162
+ }
163
+ console.log();
164
+ }
165
+ else {
166
+ console.log(dim(" .context/activeContext.md"));
167
+ console.log(` ${severityIcon("fail")} File missing (required)`);
168
+ failCount++;
169
+ console.log();
170
+ }
171
+ // Lint systemPatterns.md
172
+ const spPath = join(cwd, ".context", "systemPatterns.md");
173
+ if (existsSync(spPath)) {
174
+ console.log(dim(" .context/systemPatterns.md"));
175
+ const content = readFileSync(spPath, "utf-8");
176
+ const results = lintSystemPatterns(content);
177
+ for (const r of results) {
178
+ printResult(r);
179
+ countResult(r.severity);
180
+ }
181
+ console.log();
182
+ }
183
+ // Lint TOCKET.md
184
+ console.log(dim(" TOCKET.md"));
185
+ const tocketContent = readFileSync(tocketPath, "utf-8");
186
+ const tocketResults = lintProtocolSpec(tocketContent);
187
+ for (const r of tocketResults) {
188
+ printResult(r);
189
+ countResult(r.severity);
190
+ }
191
+ console.log();
192
+ // Lint agent configs
193
+ const agentFiles = ["CLAUDE.md", "GEMINI.md"];
194
+ for (const af of agentFiles) {
195
+ const afPath = join(cwd, af);
196
+ if (existsSync(afPath)) {
197
+ console.log(dim(` ${af}`));
198
+ const content = readFileSync(afPath, "utf-8");
199
+ const results = lintAgentConfig(af, content);
200
+ for (const r of results) {
201
+ printResult(r);
202
+ countResult(r.severity);
203
+ }
204
+ console.log();
205
+ }
206
+ }
207
+ // Uncommitted context check
208
+ if (isGitRepo(cwd)) {
209
+ try {
210
+ const output = execSync("git status --porcelain .context/", {
211
+ cwd,
212
+ encoding: "utf-8",
213
+ }).trim();
214
+ if (output) {
215
+ const count = output.split("\n").length;
216
+ console.log(dim(" git"));
217
+ const r = {
218
+ severity: "info",
219
+ message: `${count} uncommitted change(s) in .context/`,
220
+ suggestion: "Commit .context/ changes to preserve shared memory",
221
+ };
222
+ printResult(r);
223
+ countResult(r.severity);
224
+ console.log();
225
+ }
226
+ }
227
+ catch {
228
+ // git error — skip
229
+ }
230
+ }
231
+ // Summary
232
+ const total = passCount + warnCount + infoCount + failCount;
233
+ console.log(dim(` ${passCount} passed, ${warnCount} warnings, ${infoCount} info, ${failCount} failures (${total} checks)`));
234
+ console.log();
235
+ if (failCount > 0) {
236
+ console.log(themeFail("Context has issues that need attention."));
237
+ process.exitCode = 1;
238
+ }
239
+ else if (warnCount > 0) {
240
+ console.log(themeWarn("Context is functional but could be improved."));
241
+ }
242
+ else {
243
+ console.log(themePass("Context is in great shape."));
244
+ }
245
+ function printResult(r) {
246
+ const icon = severityIcon(r.severity);
247
+ console.log(` ${icon} ${r.message}`);
248
+ if (r.suggestion) {
249
+ console.log(` ${dim(r.suggestion)}`);
250
+ }
251
+ }
252
+ function countResult(severity) {
253
+ switch (severity) {
254
+ case "pass":
255
+ passCount++;
256
+ break;
257
+ case "warn":
258
+ warnCount++;
259
+ break;
260
+ case "info":
261
+ infoCount++;
262
+ break;
263
+ case "fail":
264
+ failCount++;
265
+ break;
266
+ }
267
+ }
268
+ });
269
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerStatusCommand(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { success, warn, info, dim, heading } from "../utils/theme.js";
4
+ import { getCurrentBranch, isGitRepo } from "../utils/git.js";
5
+ function extractFocus(content) {
6
+ const match = content.match(/## Current Focus\s*\n+(.+)/);
7
+ if (!match?.[1])
8
+ return "";
9
+ const line = match[1].trim();
10
+ if (line.startsWith("_") || line.includes("No active tasks"))
11
+ return "";
12
+ return line.length > 80 ? line.substring(0, 77) + "..." : line;
13
+ }
14
+ function daysSince(filePath) {
15
+ const stats = statSync(filePath);
16
+ return Math.floor((Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24));
17
+ }
18
+ function countContextFiles(contextDir) {
19
+ const expected = [
20
+ "activeContext.md",
21
+ "systemPatterns.md",
22
+ "productContext.md",
23
+ "techContext.md",
24
+ "progress.md",
25
+ ];
26
+ return expected.filter((f) => existsSync(join(contextDir, f))).length;
27
+ }
28
+ export function registerStatusCommand(program) {
29
+ program
30
+ .command("status")
31
+ .description("Show a quick overview of the current workspace")
32
+ .action(() => {
33
+ const cwd = process.cwd();
34
+ const contextDir = join(cwd, ".context");
35
+ const hasWorkspace = existsSync(contextDir);
36
+ console.log(heading("\n Tocket Status\n"));
37
+ // Workspace
38
+ if (!hasWorkspace) {
39
+ console.log(" " + warn("No Tocket workspace in this directory."));
40
+ console.log(" " + dim("Run 'tocket init' to get started.\n"));
41
+ return;
42
+ }
43
+ const fileCount = countContextFiles(contextDir);
44
+ console.log(" " + success(`Workspace: ${fileCount}/5 context files`));
45
+ // Focus
46
+ const activeContextPath = join(contextDir, "activeContext.md");
47
+ if (existsSync(activeContextPath)) {
48
+ const content = readFileSync(activeContextPath, "utf-8");
49
+ const focus = extractFocus(content);
50
+ if (focus) {
51
+ console.log(" " + info(`Focus: ${focus}`));
52
+ }
53
+ else {
54
+ console.log(" " + dim(" Focus: (none set)"));
55
+ }
56
+ const days = daysSince(activeContextPath);
57
+ if (days > 7) {
58
+ console.log(" " + warn(`Context last updated ${days} days ago`));
59
+ }
60
+ }
61
+ // Git
62
+ if (isGitRepo(cwd)) {
63
+ const branch = getCurrentBranch(cwd);
64
+ if (branch) {
65
+ console.log(" " + info(`Branch: ${branch}`));
66
+ }
67
+ }
68
+ // Agent configs
69
+ const agents = ["CLAUDE.md", "GEMINI.md", ".cursorrules"].filter((f) => existsSync(join(cwd, f)));
70
+ if (agents.length > 0) {
71
+ console.log(" " + info(`Agents: ${agents.join(", ")}`));
72
+ }
73
+ // Last sync
74
+ const progressPath = join(contextDir, "progress.md");
75
+ if (existsSync(progressPath)) {
76
+ const progressContent = readFileSync(progressPath, "utf-8");
77
+ const sessionMatch = progressContent.match(/## Session: (\d{4}-\d{2}-\d{2})/g);
78
+ if (sessionMatch && sessionMatch.length > 0) {
79
+ const lastSession = sessionMatch[sessionMatch.length - 1].replace("## Session: ", "");
80
+ console.log(" " + dim(` Last sync: ${lastSession}`));
81
+ }
82
+ }
83
+ console.log();
84
+ });
85
+ }
@@ -9,7 +9,8 @@ export function registerSyncCommand(program) {
9
9
  program
10
10
  .command("sync")
11
11
  .description("Update Memory Bank from git history and session artifacts")
12
- .action(async () => {
12
+ .option("--summary <text>", "Session summary (skip prompt)")
13
+ .action(async (options) => {
13
14
  const progressPath = join(process.cwd(), ".context", "progress.md");
14
15
  const contextDir = join(process.cwd(), ".context");
15
16
  if (!existsSync(contextDir)) {
@@ -18,7 +19,7 @@ export function registerSyncCommand(program) {
18
19
  return;
19
20
  }
20
21
  const config = await getConfig();
21
- const summary = await input({
22
+ const summary = options.summary ?? await input({
22
23
  message: "What did you accomplish in this session?",
23
24
  });
24
25
  const commits = getRecentCommitsRaw();
@@ -1,2 +1,9 @@
1
1
  import type { Command } from "commander";
2
+ export interface CheckResult {
3
+ icon: string;
4
+ message: string;
5
+ }
6
+ export declare function checkFile(basePath: string, relativePath: string, required: boolean): CheckResult;
7
+ export declare function checkStale(basePath: string, relativePath: string): CheckResult | null;
8
+ export declare function checkAgentFile(basePath: string): CheckResult;
2
9
  export declare function registerValidateCommand(program: Command): void;
@@ -4,7 +4,7 @@ import { success as themePass, warn as themeWarn, error as themeFail, heading }
4
4
  const PASS = themePass("");
5
5
  const WARN = themeWarn("");
6
6
  const FAIL = themeFail("");
7
- function checkFile(basePath, relativePath, required) {
7
+ export function checkFile(basePath, relativePath, required) {
8
8
  const fullPath = join(basePath, relativePath);
9
9
  if (existsSync(fullPath)) {
10
10
  return { icon: PASS, message: `${relativePath} found` };
@@ -14,7 +14,7 @@ function checkFile(basePath, relativePath, required) {
14
14
  }
15
15
  return { icon: WARN, message: `${relativePath} missing (optional)` };
16
16
  }
17
- function checkStale(basePath, relativePath) {
17
+ export function checkStale(basePath, relativePath) {
18
18
  const fullPath = join(basePath, relativePath);
19
19
  if (!existsSync(fullPath))
20
20
  return null;
@@ -28,7 +28,7 @@ function checkStale(basePath, relativePath) {
28
28
  }
29
29
  return null;
30
30
  }
31
- function checkAgentFile(basePath) {
31
+ export function checkAgentFile(basePath) {
32
32
  const agentFiles = [
33
33
  "CLAUDE.md",
34
34
  "GEMINI.md",
package/dist/index.js CHANGED
@@ -7,6 +7,11 @@ import { registerGenerateCommand } from "./commands/generate.cmd.js";
7
7
  import { registerSyncCommand } from "./commands/sync.cmd.js";
8
8
  import { registerValidateCommand } from "./commands/validate.cmd.js";
9
9
  import { registerConfigCommand } from "./commands/config.cmd.js";
10
+ import { registerEjectCommand } from "./commands/eject.cmd.js";
11
+ import { registerFocusCommand } from "./commands/focus.cmd.js";
12
+ import { registerStatusCommand } from "./commands/status.cmd.js";
13
+ import { registerDoctorCommand } from "./commands/doctor.cmd.js";
14
+ import { registerLintCommand } from "./commands/lint.cmd.js";
10
15
  const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
11
16
  const program = new Command();
12
17
  program
@@ -18,6 +23,11 @@ registerGenerateCommand(program);
18
23
  registerSyncCommand(program);
19
24
  registerValidateCommand(program);
20
25
  registerConfigCommand(program);
26
+ registerEjectCommand(program);
27
+ registerFocusCommand(program);
28
+ registerStatusCommand(program);
29
+ registerDoctorCommand(program);
30
+ registerLintCommand(program);
21
31
  // No-args: show interactive dashboard (TTY) or help (non-TTY)
22
32
  const args = process.argv.slice(2);
23
33
  if (args.length === 0 && process.stdin.isTTY) {
@@ -0,0 +1 @@
1
+ export {};