tack-cli 0.1.0 → 0.1.2

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/dist/index.js CHANGED
@@ -17,18 +17,19 @@ import { runWatchPlain } from "./plain/watch.js";
17
17
  import { log, readRecentLogs } from "./lib/logger.js";
18
18
  import { appendDecision, normalizeDecisionActor, readDecisionsMarkdown } from "./engine/decisions.js";
19
19
  import { ensureTackIntegrity } from "./lib/files.js";
20
- import { fileExists } from "./lib/files.js";
21
20
  import { readSpecWithError, specExists } from "./lib/files.js";
21
+ import { getDefaultCommand } from "./lib/cli.js";
22
22
  import { printNotes, addNotePlain } from "./plain/notes.js";
23
23
  import { compactNotes } from "./lib/notes.js";
24
24
  import { runDiffPlain } from "./plain/diff.js";
25
- const ASCII_LOGO = `
26
- ████████╗ █████╗ ██████╗██╗ ██╗
27
- ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
28
- ██║ ███████║██║ █████╔╝
29
- ██║ ██╔══██║██║ ██╔═██╗
30
- ██║ ██║ ██║╚██████╗██║ ██╗
31
- ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
25
+ import { formatMissingTackContextMessage, tackDirExists } from "./lib/files.js";
26
+ const ASCII_LOGO = `
27
+ ████████╗ █████╗ ██████╗██╗ ██╗
28
+ ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
29
+ ██║ ███████║██║ █████╔╝
30
+ ██║ ██╔══██║██║ ██╔═██╗
31
+ ██║ ██║ ██║╚██████╗██║ ██╗
32
+ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
32
33
  `;
33
34
  import updateNotifier from "update-notifier";
34
35
  import { readFileSync } from "node:fs";
@@ -42,41 +43,45 @@ if (args.version || args.v) {
42
43
  process.exit(0);
43
44
  }
44
45
  const rawCommand = args._[0];
45
- const command = rawCommand ?? (fileExists(".tack") ? "watch" : "init");
46
+ const command = rawCommand ?? getDefaultCommand();
46
47
  const VALID_COMMANDS = ["init", "status", "watch", "handoff", "log", "note", "diff", "mcp", "help"];
47
48
  function isValidCommand(value) {
48
49
  return VALID_COMMANDS.includes(value);
49
50
  }
50
51
  if (command === "help" || args.help || args.h) {
51
52
  // eslint-disable-next-line no-console
52
- console.log(`
53
- ${ASCII_LOGO}
54
- tack — Architecture drift guard
55
-
56
- Usage:
57
- npx tack init [--ink] Set up spec.yaml from detected architecture
58
- npx tack status [--ink] Run a scan and show current state
59
- npx tack watch [--plain] Persistent watcher with live drift alerts
60
- npx tack handoff [--ink] Generate agent handoff artifacts
61
- npx tack log View or append decisions
62
- npx tack log events [N] Show last N log events (default 50)
63
- npx tack note View/add agent notes
64
- npx tack diff <base-branch> Compare architecture vs base branch (plain)
65
- npx tack mcp Start MCP server (for Cursor / agent integrations)
66
- npx tack help Show this help text
67
-
68
- Output mode:
69
- default: plain output for all commands except watch
70
- --ink: force Ink UI for init/status/handoff
71
- --plain or TACK_PLAIN=1: force plain output (including watch)
72
-
73
- Files (all in .tack/):
74
- spec.yaml Your declared architecture contract
75
- _audit.yaml Latest detector sweep results
76
- _drift.yaml Current unresolved drift items
77
- _logs.ndjson Append-only event log
78
- context.md, goals.md, assumptions.md, open_questions.md
79
- handoffs/<ts>.md, handoffs/<ts>.json
53
+ console.log(`
54
+ ${ASCII_LOGO}
55
+ tack — Architecture drift guard
56
+
57
+ Usage:
58
+ npx tack init [--ink] Set up spec.yaml from detected architecture
59
+ npx tack status [--ink] Run a scan and show current state
60
+ npx tack watch [--plain] Persistent watcher with live drift alerts
61
+ npx tack handoff [--ink] Generate agent handoff artifacts
62
+ npx tack log View or append decisions
63
+ npx tack log events [N] Show last N log events (default 50)
64
+ npx tack note View/add agent notes
65
+ npx tack diff <base-branch> Compare architecture vs base branch (plain)
66
+ npx tack mcp Start MCP server (for Cursor / agent integrations)
67
+ npx tack help Show this help text
68
+
69
+ Output mode:
70
+ default: plain output for all commands except watch
71
+ --ink: force Ink UI for init/status/handoff
72
+ --plain or TACK_PLAIN=1: force plain output (including watch)
73
+
74
+ Files (all in .tack/):
75
+ spec.yaml Your declared architecture contract
76
+ _audit.yaml Latest detector sweep results
77
+ _drift.yaml Current unresolved drift items
78
+ _logs.ndjson Append-only event log
79
+ context.md, goals.md, assumptions.md, open_questions.md
80
+ handoffs/<ts>.md, handoffs/<ts>.json
81
+
82
+ Project root:
83
+ Existing Tack project: nearest ancestor directory that contains .tack/
84
+ New project: cd to the intended project root, then run "tack init"
80
85
  `);
81
86
  process.exit(0);
82
87
  }
@@ -86,14 +91,14 @@ if (!isValidCommand(command)) {
86
91
  process.exit(1);
87
92
  }
88
93
  const normalizedCommand = command;
94
+ const commandsRequiringExistingTack = new Set(["status", "watch", "handoff", "log", "note", "diff", "mcp"]);
95
+ if (commandsRequiringExistingTack.has(normalizedCommand) && !tackDirExists()) {
96
+ // eslint-disable-next-line no-console
97
+ console.error(formatMissingTackContextMessage(normalizedCommand));
98
+ process.exit(1);
99
+ }
89
100
  // tack mcp: start the MCP server (stdio). Run from a project root that has .tack/
90
101
  if (normalizedCommand === "mcp") {
91
- const cwdTack = path.join(process.cwd(), ".tack");
92
- if (!existsSync(cwdTack)) {
93
- // eslint-disable-next-line no-console
94
- console.error("Run `tack mcp` from a project root that contains a .tack/ directory.");
95
- process.exit(1);
96
- }
97
102
  const dir = path.dirname(fileURLToPath(import.meta.url));
98
103
  const mcpHere = path.join(dir, "mcp.js");
99
104
  const mcpInDist = path.join(dir, "..", "dist", "mcp.js");
package/dist/lib/cli.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  /**
2
- * Default command resolution: no-arg CLI runs init when .tack/ is missing, otherwise watch.
2
+ * Returns the default command when `tack` is invoked with no arguments:
3
+ * - "init" when .tack/ does not exist
4
+ * - "watch" when .tack/ exists
3
5
  */
4
- export declare function resolveDefaultCommand(raw: string | undefined, tackDirExists: boolean): "init" | "watch" | string;
6
+ export declare function getDefaultCommand(tackExists?: () => boolean): "init" | "watch";
package/dist/lib/cli.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { tackDirExists } from "./files.js";
1
2
  /**
2
- * Default command resolution: no-arg CLI runs init when .tack/ is missing, otherwise watch.
3
+ * Returns the default command when `tack` is invoked with no arguments:
4
+ * - "init" when .tack/ does not exist
5
+ * - "watch" when .tack/ exists
3
6
  */
4
- export function resolveDefaultCommand(raw, tackDirExists) {
5
- if (raw !== undefined && raw !== "")
6
- return raw;
7
- return tackDirExists ? "watch" : "init";
7
+ export function getDefaultCommand(tackExists = tackDirExists) {
8
+ return tackExists() ? "watch" : "init";
8
9
  }
@@ -1,6 +1,9 @@
1
1
  import { type Spec, type Audit, type DriftState } from "./signals.js";
2
2
  export declare function projectRoot(): string;
3
3
  export declare function findProjectRoot(): string;
4
+ /** True when the .tack/ directory exists (used for default CLI behavior). */
5
+ export declare function tackDirExists(): boolean;
6
+ export declare function formatMissingTackContextMessage(command: string): string;
4
7
  export declare function writeSafe(filepath: string, content: string): void;
5
8
  export declare function appendSafe(filepath: string, content: string): void;
6
9
  export declare function ensureTackDir(): void;
package/dist/lib/files.js CHANGED
@@ -6,19 +6,77 @@ import { safeLoadYaml } from "./yaml.js";
6
6
  import { validateAudit, validateDriftState, validateSpec } from "./validate.js";
7
7
  const LEGACY_DIRNAME = "tack";
8
8
  const TACK_DIRNAME = ".tack";
9
- export function projectRoot() {
10
- const cwd = path.resolve(process.cwd());
11
- let current = cwd;
9
+ const LEGACY_TACK_MARKERS = [
10
+ "spec.yaml",
11
+ "audit.yaml",
12
+ "drift.yaml",
13
+ "logs.ndjson",
14
+ "context.md",
15
+ "goals.md",
16
+ "assumptions.md",
17
+ "open_questions.md",
18
+ "decisions.md",
19
+ "implementation_status.md",
20
+ "verification.md",
21
+ "handoffs",
22
+ ];
23
+ const PROJECT_MARKERS = [
24
+ ".git",
25
+ "package.json",
26
+ "README.md",
27
+ "src",
28
+ "node_modules",
29
+ "backlog",
30
+ "dist",
31
+ ];
32
+ function looksLikeLegacyTackDir(dir) {
33
+ try {
34
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
35
+ return false;
36
+ }
37
+ const entries = new Set(fs.readdirSync(dir));
38
+ const hasLegacyMarkers = LEGACY_TACK_MARKERS.some((name) => entries.has(name));
39
+ if (!hasLegacyMarkers) {
40
+ return false;
41
+ }
42
+ const hasProjectMarkers = PROJECT_MARKERS.some((name) => entries.has(name));
43
+ return !hasProjectMarkers;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ function findNearestProjectRootWithContext(start = process.cwd()) {
50
+ let current = path.resolve(start);
51
+ if (path.basename(current) === TACK_DIRNAME) {
52
+ current = path.dirname(current);
53
+ }
54
+ else if (path.basename(current) === LEGACY_DIRNAME && looksLikeLegacyTackDir(current)) {
55
+ current = path.dirname(current);
56
+ }
12
57
  while (true) {
13
- if (path.basename(current) === TACK_DIRNAME) {
14
- return path.dirname(current);
58
+ const tackDir = path.join(current, TACK_DIRNAME);
59
+ try {
60
+ if (fs.existsSync(tackDir) && fs.statSync(tackDir).isDirectory()) {
61
+ return current;
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore stat failures and keep walking upward.
66
+ }
67
+ const legacyDir = path.join(current, LEGACY_DIRNAME);
68
+ if (looksLikeLegacyTackDir(legacyDir)) {
69
+ return current;
15
70
  }
16
71
  const parent = path.dirname(current);
17
- if (parent === current)
18
- break;
72
+ if (parent === current) {
73
+ return null;
74
+ }
19
75
  current = parent;
20
76
  }
21
- return cwd;
77
+ }
78
+ export function projectRoot() {
79
+ return findNearestProjectRootWithContext() ?? path.resolve(process.cwd());
22
80
  }
23
81
  export function findProjectRoot() {
24
82
  return projectRoot();
@@ -29,6 +87,10 @@ function getLegacyTackDir() {
29
87
  function getTackDir() {
30
88
  return path.resolve(projectRoot(), TACK_DIRNAME);
31
89
  }
90
+ /** True when the .tack/ directory exists (used for default CLI behavior). */
91
+ export function tackDirExists() {
92
+ return findNearestProjectRootWithContext() !== null;
93
+ }
32
94
  function emitValidationWarnings(file, warnings) {
33
95
  if (warnings.length === 0)
34
96
  return;
@@ -39,10 +101,17 @@ function emitValidationWarnings(file, warnings) {
39
101
  function migrateLegacyDirIfNeeded() {
40
102
  const legacyDir = getLegacyTackDir();
41
103
  const newDir = getTackDir();
42
- if (!fs.existsSync(newDir) && fs.existsSync(legacyDir)) {
104
+ if (!fs.existsSync(newDir) && looksLikeLegacyTackDir(legacyDir)) {
43
105
  fs.renameSync(legacyDir, newDir);
44
106
  }
45
107
  }
108
+ export function formatMissingTackContextMessage(command) {
109
+ return [
110
+ `No .tack/ directory was found for \`${command}\`.`,
111
+ "Run Tack from your project root (the directory that contains .tack/).",
112
+ "If this is a new project, cd to the intended root and run `tack init` first.",
113
+ ].join(" ");
114
+ }
46
115
  function migrateMachineFilesIfNeeded() {
47
116
  const mapping = [
48
117
  { oldName: "audit.yaml", newName: "_audit.yaml" },
@@ -1,3 +1,15 @@
1
1
  import type { LogEvent, LogEventInput } from "./signals.js";
2
+ export type McpActivityEvent = Extract<LogEvent, {
3
+ event: "mcp:resource" | "mcp:tool";
4
+ }>;
5
+ export type McpActivityNotice = {
6
+ event: McpActivityEvent;
7
+ message: string;
8
+ };
2
9
  export declare function log(event: LogEventInput): void;
3
10
  export declare function readRecentLogs<T = LogEvent>(limit?: number): T[];
11
+ export declare function isMcpActivityEvent(event: LogEvent): event is McpActivityEvent;
12
+ export declare function readRecentMcpActivity(limit?: number): McpActivityEvent[];
13
+ export declare function mcpActivityEventKey(event: McpActivityEvent): string;
14
+ export declare function formatMcpActivityEvent(event: McpActivityEvent): string;
15
+ export declare function createMcpActivityMonitor(): () => McpActivityNotice[];
@@ -1,7 +1,8 @@
1
1
  import { appendSafe, logsPath } from "./files.js";
2
- import { rotateNdjsonFile, safeReadNdjson } from "./ndjson.js";
2
+ import { createNdjsonTailReader, rotateNdjsonFile, safeReadNdjson } from "./ndjson.js";
3
3
  const LOG_MAX_BYTES = 5 * 1024 * 1024;
4
4
  const LOG_KEEP_LINES = 5000;
5
+ const MCP_ACTIVITY_SUPPRESS_MS = 1500;
5
6
  export function log(event) {
6
7
  const entry = {
7
8
  ts: new Date().toISOString(),
@@ -19,3 +20,54 @@ export function log(event) {
19
20
  export function readRecentLogs(limit = 50) {
20
21
  return safeReadNdjson(logsPath(), limit);
21
22
  }
23
+ export function isMcpActivityEvent(event) {
24
+ return event.event === "mcp:resource" || event.event === "mcp:tool";
25
+ }
26
+ export function readRecentMcpActivity(limit = 50) {
27
+ return readRecentLogs(limit).filter(isMcpActivityEvent);
28
+ }
29
+ export function mcpActivityEventKey(event) {
30
+ return `${event.ts}:${event.event}:${event.event === "mcp:resource" ? event.resource : event.tool}`;
31
+ }
32
+ function formatMcpResourceName(resource) {
33
+ if (!resource.startsWith("tack://")) {
34
+ return resource;
35
+ }
36
+ return resource.replace(/^tack:\/\//, "");
37
+ }
38
+ export function formatMcpActivityEvent(event) {
39
+ if (event.event === "mcp:resource") {
40
+ return `MCP read ${formatMcpResourceName(event.resource)}`;
41
+ }
42
+ return `MCP called ${event.tool}`;
43
+ }
44
+ export function createMcpActivityMonitor() {
45
+ const seen = new Set(safeReadNdjson(logsPath()).filter(isMcpActivityEvent).map(mcpActivityEventKey));
46
+ const lastShownAt = new Map();
47
+ const readNewLogEvents = createNdjsonTailReader(logsPath());
48
+ return () => {
49
+ const notices = [];
50
+ for (const event of readNewLogEvents()) {
51
+ if (!isMcpActivityEvent(event))
52
+ continue;
53
+ const key = mcpActivityEventKey(event);
54
+ if (seen.has(key))
55
+ continue;
56
+ seen.add(key);
57
+ const kind = event.event === "mcp:resource" ? `resource:${event.resource}` : `tool:${event.tool}`;
58
+ const tsMs = Date.parse(event.ts);
59
+ const lastMs = lastShownAt.get(kind) ?? 0;
60
+ if (Number.isFinite(tsMs) && tsMs - lastMs < MCP_ACTIVITY_SUPPRESS_MS) {
61
+ continue;
62
+ }
63
+ if (Number.isFinite(tsMs)) {
64
+ lastShownAt.set(kind, tsMs);
65
+ }
66
+ notices.push({
67
+ event,
68
+ message: formatMcpActivityEvent(event),
69
+ });
70
+ }
71
+ return notices;
72
+ };
73
+ }
@@ -1,2 +1,3 @@
1
1
  export declare function safeReadNdjson<T = Record<string, unknown>>(filepath: string, limit?: number): T[];
2
+ export declare function createNdjsonTailReader<T = Record<string, unknown>>(filepath: string): () => T[];
2
3
  export declare function rotateNdjsonFile(filepath: string, maxBytes: number, keepLines: number): void;
@@ -1,4 +1,18 @@
1
1
  import * as fs from "node:fs";
2
+ function parseNdjsonLines(lines) {
3
+ const out = [];
4
+ for (const line of lines) {
5
+ if (line.trim().length === 0)
6
+ continue;
7
+ try {
8
+ out.push(JSON.parse(line));
9
+ }
10
+ catch {
11
+ continue;
12
+ }
13
+ }
14
+ return out;
15
+ }
2
16
  export function safeReadNdjson(filepath, limit) {
3
17
  if (!fs.existsSync(filepath))
4
18
  return [];
@@ -6,21 +20,67 @@ export function safeReadNdjson(filepath, limit) {
6
20
  const raw = fs.readFileSync(filepath, "utf-8");
7
21
  const lines = raw.split("\n").filter((line) => line.trim().length > 0);
8
22
  const slice = limit ? lines.slice(-limit) : lines;
9
- const out = [];
10
- for (const line of slice) {
11
- try {
12
- out.push(JSON.parse(line));
13
- }
14
- catch {
15
- continue;
16
- }
17
- }
18
- return out;
23
+ return parseNdjsonLines(slice);
19
24
  }
20
25
  catch {
21
26
  return [];
22
27
  }
23
28
  }
29
+ export function createNdjsonTailReader(filepath) {
30
+ let offset = 0;
31
+ let remainder = "";
32
+ try {
33
+ offset = fs.existsSync(filepath) ? fs.statSync(filepath).size : 0;
34
+ }
35
+ catch {
36
+ offset = 0;
37
+ }
38
+ return () => {
39
+ if (!fs.existsSync(filepath)) {
40
+ offset = 0;
41
+ remainder = "";
42
+ return [];
43
+ }
44
+ let stat;
45
+ try {
46
+ stat = fs.statSync(filepath);
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ const start = stat.size < offset ? 0 : offset;
52
+ const length = stat.size - start;
53
+ if (length <= 0) {
54
+ offset = stat.size;
55
+ return [];
56
+ }
57
+ let fd;
58
+ try {
59
+ fd = fs.openSync(filepath, "r");
60
+ const buffer = Buffer.alloc(length);
61
+ fs.readSync(fd, buffer, 0, length, start);
62
+ offset = stat.size;
63
+ const chunk = remainder + buffer.toString("utf-8");
64
+ const endsWithNewline = chunk.endsWith("\n");
65
+ const lines = chunk.split("\n");
66
+ remainder = endsWithNewline ? "" : (lines.pop() ?? "");
67
+ return parseNdjsonLines(lines);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ finally {
73
+ if (fd !== undefined) {
74
+ try {
75
+ fs.closeSync(fd);
76
+ }
77
+ catch {
78
+ // Ignore close failures.
79
+ }
80
+ }
81
+ }
82
+ };
83
+ }
24
84
  export function rotateNdjsonFile(filepath, maxBytes, keepLines) {
25
85
  if (!fs.existsSync(filepath))
26
86
  return;
@@ -95,6 +95,14 @@ export type LogEvent = {
95
95
  systems_detected: number;
96
96
  drift_items: number;
97
97
  duration_ms: number;
98
+ } | {
99
+ ts: string;
100
+ event: "mcp:resource";
101
+ resource: string;
102
+ } | {
103
+ ts: string;
104
+ event: "mcp:tool";
105
+ tool: string;
98
106
  } | {
99
107
  ts: string;
100
108
  event: "drift:detected";
package/dist/mcp.js CHANGED
@@ -42,7 +42,7 @@ function latestHandoffJsonPath() {
42
42
  async function main() {
43
43
  const server = new McpServer({
44
44
  name: "tack-mcp",
45
- version: "0.1.0",
45
+ version: "0.1.2",
46
46
  }, {});
47
47
  // Intent: high-level purpose and goals, without architecture internals.
48
48
  server.registerResource("intent", "tack://context/intent", {
@@ -50,6 +50,7 @@ async function main() {
50
50
  description: "High-level North Star, goals, and open questions for this project.",
51
51
  mimeType: "text/markdown",
52
52
  }, async (uri) => {
53
+ log({ event: "mcp:resource", resource: uri.href });
53
54
  const parts = [];
54
55
  const context = safeReadFile(contextPath());
55
56
  if (context) {
@@ -86,6 +87,7 @@ async function main() {
86
87
  description: "Binary, source-anchored implementation status and architecture spec.",
87
88
  mimeType: "text/markdown",
88
89
  }, async (uri) => {
90
+ log({ event: "mcp:resource", resource: uri.href });
89
91
  const parts = [];
90
92
  const impl = safeReadFile(implementationStatusPath());
91
93
  if (impl) {
@@ -114,6 +116,7 @@ async function main() {
114
116
  description: "Latest handoff JSON generated by `tack handoff`.",
115
117
  mimeType: "application/json",
116
118
  }, async (uri) => {
119
+ log({ event: "mcp:resource", resource: uri.href });
117
120
  const jsonPath = latestHandoffJsonPath();
118
121
  if (!jsonPath) {
119
122
  return {
@@ -141,6 +144,7 @@ async function main() {
141
144
  description: "Recent architecture and product decisions driving this project.",
142
145
  mimeType: "text/markdown",
143
146
  }, async (uri) => {
147
+ log({ event: "mcp:resource", resource: uri.href });
144
148
  const pack = parseContextPack();
145
149
  const recent = pack.decisions.slice(-10);
146
150
  if (recent.length === 0) {
@@ -174,6 +178,7 @@ async function main() {
174
178
  description: "Raw machine state from _audit.yaml and _drift.yaml.",
175
179
  mimeType: "text/markdown",
176
180
  }, async (uri) => {
181
+ log({ event: "mcp:resource", resource: uri.href });
177
182
  const parts = [];
178
183
  const audit = safeReadFile(auditPath());
179
184
  if (audit) {
@@ -205,6 +210,7 @@ async function main() {
205
210
  actor: z.string().optional(),
206
211
  }),
207
212
  }, async (args) => {
213
+ log({ event: "mcp:tool", tool: "log_decision" });
208
214
  const decision = args.decision;
209
215
  const reasoning = args.reasoning;
210
216
  const actor = typeof args.actor === "string" ? args.actor : undefined;
@@ -233,6 +239,7 @@ async function main() {
233
239
  related_files: z.array(z.string()).optional(),
234
240
  }),
235
241
  }, async (args) => {
242
+ log({ event: "mcp:tool", tool: "log_agent_note" });
236
243
  const actor = args.actor && args.actor.trim().length > 0 ? args.actor : "user";
237
244
  const ok = addNote({
238
245
  type: args.type,
@@ -1,5 +1,9 @@
1
1
  export declare function green(text: string): string;
2
2
  export declare function red(text: string): string;
3
3
  export declare function blue(text: string): string;
4
+ export declare function cyan(text: string): string;
5
+ export declare function yellow(text: string): string;
4
6
  export declare function gray(text: string): string;
5
7
  export declare function bold(text: string): string;
8
+ export declare function checkBadge(): string;
9
+ export declare function mcpBadge(): string;
@@ -8,9 +8,21 @@ export function red(text) {
8
8
  export function blue(text) {
9
9
  return pc.blue(text);
10
10
  }
11
+ export function cyan(text) {
12
+ return pc.cyan(text);
13
+ }
14
+ export function yellow(text) {
15
+ return pc.yellow(text);
16
+ }
11
17
  export function gray(text) {
12
18
  return pc.gray(text);
13
19
  }
14
20
  export function bold(text) {
15
21
  return pc.bold(text);
16
22
  }
23
+ export function checkBadge() {
24
+ return pc.bgYellow(pc.black(" CHECK "));
25
+ }
26
+ export function mcpBadge() {
27
+ return pc.bgCyan(pc.black(" MCP "));
28
+ }
@@ -1,7 +1,9 @@
1
1
  import chokidar from "chokidar";
2
2
  import * as path from "node:path";
3
+ import { logsPath } from "../lib/files.js";
3
4
  import { runStatusScan } from "../engine/status.js";
4
- import { blue, gray, green, red } from "./colors.js";
5
+ import { createMcpActivityMonitor } from "../lib/logger.js";
6
+ import { blue, checkBadge, gray, green, mcpBadge, red, yellow } from "./colors.js";
5
7
  const IGNORE_PATTERNS = [
6
8
  "**/node_modules/**",
7
9
  "**/.git/**",
@@ -25,7 +27,7 @@ function printSnapshot(reason) {
25
27
  }
26
28
  const ts = new Date().toISOString();
27
29
  const healthy = result.status.health === "aligned";
28
- console.log(`${blue(`[${ts}]`)} ${reason} :: health=${healthy ? green("aligned") : red("drift")} drift=${result.status.driftCount > 0 ? red(String(result.status.driftCount)) : green("0")}`);
30
+ console.log(`${checkBadge()} ${blue(`[${ts}]`)} ${yellow(reason)} :: health=${healthy ? green("aligned") : red("drift")} drift=${result.status.driftCount > 0 ? red(String(result.status.driftCount)) : green("0")}`);
29
31
  for (const item of result.status.driftItems.slice(0, 5)) {
30
32
  console.log(` - ${red(item.system)}: ${item.message}`);
31
33
  }
@@ -38,7 +40,7 @@ export async function runWatchPlain() {
38
40
  const ok = printSnapshot("initial");
39
41
  if (!ok)
40
42
  return;
41
- console.log(`${gray("Watching for changes (plain mode). Press Ctrl+C to stop.")}`);
43
+ console.log(`${gray("Watching for changes and MCP activity (plain mode). Press Ctrl+C to stop.")}`);
42
44
  const watcher = chokidar.watch(".", {
43
45
  ignored: IGNORE_PATTERNS,
44
46
  persistent: true,
@@ -48,6 +50,15 @@ export async function runWatchPlain() {
48
50
  pollInterval: 50,
49
51
  },
50
52
  });
53
+ const logsWatcher = chokidar.watch(logsPath(), {
54
+ persistent: true,
55
+ ignoreInitial: true,
56
+ awaitWriteFinish: {
57
+ stabilityThreshold: 100,
58
+ pollInterval: 50,
59
+ },
60
+ });
61
+ const readNewMcpActivity = createMcpActivityMonitor();
51
62
  let debounceTimer = null;
52
63
  const shutdown = async () => {
53
64
  if (debounceTimer) {
@@ -55,8 +66,14 @@ export async function runWatchPlain() {
55
66
  debounceTimer = null;
56
67
  }
57
68
  await watcher.close();
69
+ await logsWatcher.close();
58
70
  console.log(gray("Stopped watch mode."));
59
71
  };
72
+ logsWatcher.on("change", () => {
73
+ for (const notice of readNewMcpActivity()) {
74
+ console.log(`${mcpBadge()} ${blue(`[${notice.event.ts}]`)} ${gray(notice.message)}`);
75
+ }
76
+ });
60
77
  watcher.on("all", (event, filepath) => {
61
78
  if (filepath.includes(`${path.sep}.tack${path.sep}`))
62
79
  return;
package/dist/ui/Logo.js CHANGED
@@ -9,5 +9,5 @@ const LOGO = `
9
9
  ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
10
10
  `;
11
11
  export function Logo() {
12
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: LOGO }), _jsx(Text, { dimColor: true, children: " Architecture drift guard \u2022 v0.1.0" })] }));
12
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: LOGO }), _jsx(Text, { dimColor: true, children: " Architecture drift guard \u2022 v0.1.2" })] }));
13
13
  }