@pedrocivita/tocket 2.0.0 → 2.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/README.md CHANGED
@@ -43,6 +43,12 @@ npx @pedrocivita/tocket generate
43
43
 
44
44
  # Sync session progress into Memory Bank
45
45
  npx @pedrocivita/tocket sync
46
+
47
+ # Update the current focus
48
+ npx @pedrocivita/tocket focus "Refactoring payment module"
49
+
50
+ # Remove all Tocket files (with confirmation)
51
+ npx @pedrocivita/tocket eject
46
52
  ```
47
53
 
48
54
  ## Commands
@@ -54,7 +60,10 @@ npx @pedrocivita/tocket sync
54
60
  | `tocket generate` | Smart payload builder — auto-fills scope from git, multi-task support |
55
61
  | `tocket sync` | Appends session summary + git log to `.context/progress.md` |
56
62
  | `tocket validate` | Checks if the current directory has a valid Tocket Memory Bank |
63
+ | `tocket focus` | Update the Current Focus in `.context/activeContext.md` |
64
+ | `tocket status` | Quick overview of workspace, focus, branch, and agents |
57
65
  | `tocket config` | Manage global settings (`~/.tocketrc.json`) |
66
+ | `tocket eject` | Remove all Tocket files from the workspace |
58
67
 
59
68
  ## Configuration
60
69
 
@@ -44,7 +44,10 @@ export async function showDashboard(program) {
44
44
  { value: "generate", name: "Generate payload" },
45
45
  { value: "sync", name: "Sync progress" },
46
46
  { value: "validate", name: "Validate workspace" },
47
+ { value: "focus", name: "Update focus" },
48
+ { value: "status", name: "Workspace status" },
47
49
  { value: "config", name: "Configure settings" },
50
+ { value: "eject", name: "Eject workspace" },
48
51
  { value: "exit", name: "Exit" },
49
52
  ]
50
53
  : [
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ /** Files created by `tocket init` that eject should remove. */
3
+ export declare const EJECT_FILES: readonly ["TOCKET.md", "CLAUDE.md", "GEMINI.md", ".cursorrules"];
4
+ /** Directories created by `tocket init` that eject should remove. */
5
+ export declare const EJECT_DIRS: readonly [".context"];
6
+ export declare function registerEjectCommand(program: Command): void;
@@ -0,0 +1,65 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+ import { existsSync } from "node:fs";
3
+ import { rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { success, warn, info, dim } from "../utils/theme.js";
6
+ /** Files created by `tocket init` that eject should remove. */
7
+ export const EJECT_FILES = [
8
+ "TOCKET.md",
9
+ "CLAUDE.md",
10
+ "GEMINI.md",
11
+ ".cursorrules",
12
+ ];
13
+ /** Directories created by `tocket init` that eject should remove. */
14
+ export const EJECT_DIRS = [".context"];
15
+ export function registerEjectCommand(program) {
16
+ program
17
+ .command("eject")
18
+ .description("Remove all Tocket files from the current workspace")
19
+ .option("-f, --force", "Skip confirmation prompt")
20
+ .action(async (options) => {
21
+ const cwd = process.cwd();
22
+ const contextDir = join(cwd, ".context");
23
+ if (!existsSync(contextDir)) {
24
+ console.log(warn("No Tocket workspace found in this directory."));
25
+ return;
26
+ }
27
+ if (!options.force) {
28
+ const ok = await confirm({
29
+ message: "This will permanently remove .context/, CLAUDE.md, GEMINI.md, TOCKET.md, and .cursorrules. Continue?",
30
+ default: false,
31
+ });
32
+ if (!ok) {
33
+ console.log(dim("\n Cancelled.\n"));
34
+ return;
35
+ }
36
+ }
37
+ let removedCount = 0;
38
+ for (const dir of EJECT_DIRS) {
39
+ const fullPath = join(cwd, dir);
40
+ if (existsSync(fullPath)) {
41
+ await rm(fullPath, { recursive: true, force: true });
42
+ console.log(info(`Removed ${dir}/`));
43
+ removedCount++;
44
+ }
45
+ }
46
+ for (const file of EJECT_FILES) {
47
+ const fullPath = join(cwd, file);
48
+ if (existsSync(fullPath)) {
49
+ await rm(fullPath, { force: true });
50
+ console.log(info(`Removed ${file}`));
51
+ removedCount++;
52
+ }
53
+ }
54
+ if (removedCount === 0) {
55
+ console.log(warn("Nothing to remove."));
56
+ }
57
+ else {
58
+ console.log("\n" +
59
+ success("Tocket workspace ejected.") +
60
+ " " +
61
+ dim("Global config (~/.tocketrc.json) was not touched.") +
62
+ "\n");
63
+ }
64
+ });
65
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from "commander";
2
+ /**
3
+ * Replaces the content under ## Current Focus with the new message.
4
+ * Exported for testing.
5
+ */
6
+ export declare function replaceFocusSection(content: string, newFocus: string): string;
7
+ export declare function registerFocusCommand(program: Command): void;
@@ -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
+ }
@@ -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
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ 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";
10
13
  const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
11
14
  const program = new Command();
12
15
  program
@@ -18,6 +21,9 @@ registerGenerateCommand(program);
18
21
  registerSyncCommand(program);
19
22
  registerValidateCommand(program);
20
23
  registerConfigCommand(program);
24
+ registerEjectCommand(program);
25
+ registerFocusCommand(program);
26
+ registerStatusCommand(program);
21
27
  // No-args: show interactive dashboard (TTY) or help (non-TTY)
22
28
  const args = process.argv.slice(2);
23
29
  if (args.length === 0 && process.stdin.isTTY) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, } from "node:fs";
4
+ import { rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { EJECT_FILES, EJECT_DIRS } from "../commands/eject.cmd.js";
8
+ describe("eject - constants", () => {
9
+ it("EJECT_FILES contains all expected files", () => {
10
+ assert.ok(EJECT_FILES.includes("TOCKET.md"));
11
+ assert.ok(EJECT_FILES.includes("CLAUDE.md"));
12
+ assert.ok(EJECT_FILES.includes("GEMINI.md"));
13
+ assert.ok(EJECT_FILES.includes(".cursorrules"));
14
+ });
15
+ it("EJECT_DIRS contains .context", () => {
16
+ assert.ok(EJECT_DIRS.includes(".context"));
17
+ });
18
+ });
19
+ describe("eject - removal logic", () => {
20
+ let tempDir;
21
+ before(() => {
22
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-eject-test-"));
23
+ });
24
+ after(() => {
25
+ rmSync(tempDir, { recursive: true, force: true });
26
+ });
27
+ it("removes .context directory recursively", async () => {
28
+ const contextDir = join(tempDir, ".context");
29
+ mkdirSync(contextDir, { recursive: true });
30
+ writeFileSync(join(contextDir, "activeContext.md"), "test", "utf-8");
31
+ writeFileSync(join(contextDir, "systemPatterns.md"), "test", "utf-8");
32
+ assert.ok(existsSync(contextDir));
33
+ await rm(contextDir, { recursive: true, force: true });
34
+ assert.ok(!existsSync(contextDir));
35
+ });
36
+ it("removes individual eject files", async () => {
37
+ for (const file of EJECT_FILES) {
38
+ const fullPath = join(tempDir, file);
39
+ writeFileSync(fullPath, "test content", "utf-8");
40
+ assert.ok(existsSync(fullPath));
41
+ await rm(fullPath, { force: true });
42
+ assert.ok(!existsSync(fullPath));
43
+ }
44
+ });
45
+ it("does not error when files are already missing", async () => {
46
+ for (const file of EJECT_FILES) {
47
+ const fullPath = join(tempDir, file);
48
+ assert.ok(!existsSync(fullPath));
49
+ await rm(fullPath, { force: true });
50
+ }
51
+ });
52
+ it("does not error when .context is already missing", async () => {
53
+ const contextDir = join(tempDir, ".context");
54
+ assert.ok(!existsSync(contextDir));
55
+ await rm(contextDir, { recursive: true, force: true });
56
+ });
57
+ });
58
+ describe("eject - partial workspace", () => {
59
+ let tempDir;
60
+ before(() => {
61
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-eject-partial-"));
62
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
63
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "data", "utf-8");
64
+ writeFileSync(join(tempDir, "TOCKET.md"), "data", "utf-8");
65
+ });
66
+ after(() => {
67
+ rmSync(tempDir, { recursive: true, force: true });
68
+ });
69
+ it("removes existing files and skips missing ones", async () => {
70
+ assert.ok(existsSync(join(tempDir, ".context")));
71
+ assert.ok(existsSync(join(tempDir, "TOCKET.md")));
72
+ assert.ok(!existsSync(join(tempDir, "CLAUDE.md")));
73
+ for (const dir of EJECT_DIRS) {
74
+ const fullPath = join(tempDir, dir);
75
+ if (existsSync(fullPath)) {
76
+ await rm(fullPath, { recursive: true, force: true });
77
+ }
78
+ }
79
+ for (const file of EJECT_FILES) {
80
+ const fullPath = join(tempDir, file);
81
+ if (existsSync(fullPath)) {
82
+ await rm(fullPath, { force: true });
83
+ }
84
+ }
85
+ assert.ok(!existsSync(join(tempDir, ".context")));
86
+ assert.ok(!existsSync(join(tempDir, "TOCKET.md")));
87
+ });
88
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { replaceFocusSection } from "../commands/focus.cmd.js";
7
+ describe("replaceFocusSection", () => {
8
+ it("replaces single-line focus content", () => {
9
+ const input = `# Active Context
10
+
11
+ ## Current Focus
12
+
13
+ Old focus text here.
14
+
15
+ ## Recent Changes
16
+
17
+ Some changes.
18
+ `;
19
+ const result = replaceFocusSection(input, "New focus message");
20
+ assert.ok(result.includes("New focus message"));
21
+ assert.ok(!result.includes("Old focus text here"));
22
+ assert.ok(result.includes("## Recent Changes"));
23
+ });
24
+ it("replaces multi-line focus content", () => {
25
+ const input = `# Active Context
26
+
27
+ ## Current Focus
28
+
29
+ **v2.0.0 — UX Overhaul.** Added purple theme, dashboard,
30
+ config command, smart generate, and 29 new tests.
31
+ All decisions resolved.
32
+
33
+ ## Recent Changes
34
+
35
+ | Date | Change |
36
+ `;
37
+ const result = replaceFocusSection(input, "Working on v2.1.0 features");
38
+ assert.ok(result.includes("Working on v2.1.0 features"));
39
+ assert.ok(!result.includes("v2.0.0"));
40
+ assert.ok(result.includes("## Recent Changes"));
41
+ });
42
+ it("preserves sections before and after Current Focus", () => {
43
+ const input = `# Active Context
44
+
45
+ <!-- comment -->
46
+
47
+ ## Current Focus
48
+
49
+ Old stuff.
50
+
51
+ ## Recent Changes
52
+
53
+ Data here.
54
+
55
+ ## Open Decisions
56
+
57
+ Decisions here.
58
+ `;
59
+ const result = replaceFocusSection(input, "Brand new focus");
60
+ assert.ok(result.includes("# Active Context"));
61
+ assert.ok(result.includes("<!-- comment -->"));
62
+ assert.ok(result.includes("## Recent Changes"));
63
+ assert.ok(result.includes("Data here."));
64
+ assert.ok(result.includes("## Open Decisions"));
65
+ assert.ok(result.includes("Decisions here."));
66
+ });
67
+ it("appends section when ## Current Focus is missing", () => {
68
+ const input = `# Active Context
69
+
70
+ ## Recent Changes
71
+
72
+ Some data.
73
+ `;
74
+ const result = replaceFocusSection(input, "New focus");
75
+ assert.ok(result.includes("## Current Focus"));
76
+ assert.ok(result.includes("New focus"));
77
+ assert.ok(result.includes("## Recent Changes"));
78
+ });
79
+ it("handles Current Focus at end of file (no following section)", () => {
80
+ const input = `# Active Context
81
+
82
+ ## Current Focus
83
+
84
+ Last section, nothing after this.
85
+ `;
86
+ const result = replaceFocusSection(input, "Updated final section");
87
+ assert.ok(result.includes("Updated final section"));
88
+ assert.ok(!result.includes("Last section"));
89
+ });
90
+ it("handles empty content between heading and next section", () => {
91
+ const input = `## Current Focus
92
+
93
+ ## Recent Changes
94
+ `;
95
+ const result = replaceFocusSection(input, "Fill the gap");
96
+ assert.ok(result.includes("Fill the gap"));
97
+ assert.ok(result.includes("## Recent Changes"));
98
+ });
99
+ });
100
+ describe("focus - file integration", () => {
101
+ let tempDir;
102
+ before(() => {
103
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-focus-test-"));
104
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
105
+ });
106
+ after(() => {
107
+ rmSync(tempDir, { recursive: true, force: true });
108
+ });
109
+ it("correctly round-trips through file read/write", () => {
110
+ const filePath = join(tempDir, ".context", "activeContext.md");
111
+ const original = `# Active Context - Test
112
+
113
+ ## Current Focus
114
+
115
+ Old focus.
116
+
117
+ ## Recent Changes
118
+
119
+ Nothing yet.
120
+ `;
121
+ writeFileSync(filePath, original, "utf-8");
122
+ const content = readFileSync(filePath, "utf-8");
123
+ const updated = replaceFocusSection(content, "Round-tripped focus");
124
+ writeFileSync(filePath, updated, "utf-8");
125
+ const final = readFileSync(filePath, "utf-8");
126
+ assert.ok(final.includes("Round-tripped focus"));
127
+ assert.ok(!final.includes("Old focus"));
128
+ assert.ok(final.includes("## Recent Changes"));
129
+ });
130
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ describe("status - workspace detection", () => {
7
+ let tempDir;
8
+ before(() => {
9
+ tempDir = mkdtempSync(join(tmpdir(), "tocket-status-test-"));
10
+ });
11
+ after(() => {
12
+ rmSync(tempDir, { recursive: true, force: true });
13
+ });
14
+ it("detects absence of .context directory", () => {
15
+ const contextDir = join(tempDir, ".context");
16
+ assert.ok(!existsSync(contextDir));
17
+ });
18
+ it("detects presence of .context directory with files", () => {
19
+ const contextDir = join(tempDir, ".context");
20
+ mkdirSync(contextDir, { recursive: true });
21
+ writeFileSync(join(contextDir, "activeContext.md"), "# Active Context\n\n## Current Focus\n\nTesting status command.\n", "utf-8");
22
+ writeFileSync(join(contextDir, "systemPatterns.md"), "test", "utf-8");
23
+ writeFileSync(join(contextDir, "progress.md"), "# Progress\n\n## Session: 2026-02-25\n\n**Summary**: test\n", "utf-8");
24
+ assert.ok(existsSync(contextDir));
25
+ assert.ok(existsSync(join(contextDir, "activeContext.md")));
26
+ });
27
+ it("counts context files correctly", () => {
28
+ const contextDir = join(tempDir, ".context");
29
+ const expected = ["activeContext.md", "systemPatterns.md", "productContext.md", "techContext.md", "progress.md"];
30
+ const count = expected.filter((f) => existsSync(join(contextDir, f))).length;
31
+ // We created 3 files above
32
+ assert.equal(count, 3);
33
+ });
34
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrocivita/tocket",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "The Context Engineering Framework for Multi-Agent Workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",