notoken-core 1.0.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 (118) hide show
  1. package/config/file-hints.json +255 -0
  2. package/config/hosts.json +14 -0
  3. package/config/intents.json +3920 -0
  4. package/config/playbooks.json +112 -0
  5. package/config/rules.json +100 -0
  6. package/dist/agents/agentSpawner.d.ts +56 -0
  7. package/dist/agents/agentSpawner.js +180 -0
  8. package/dist/agents/planner.d.ts +40 -0
  9. package/dist/agents/planner.js +175 -0
  10. package/dist/agents/playbookRunner.d.ts +45 -0
  11. package/dist/agents/playbookRunner.js +120 -0
  12. package/dist/agents/taskRunner.d.ts +61 -0
  13. package/dist/agents/taskRunner.js +142 -0
  14. package/dist/context/history.d.ts +36 -0
  15. package/dist/context/history.js +115 -0
  16. package/dist/conversation/coreference.d.ts +27 -0
  17. package/dist/conversation/coreference.js +147 -0
  18. package/dist/conversation/secrets.d.ts +43 -0
  19. package/dist/conversation/secrets.js +129 -0
  20. package/dist/conversation/store.d.ts +94 -0
  21. package/dist/conversation/store.js +184 -0
  22. package/dist/execution/git.d.ts +11 -0
  23. package/dist/execution/git.js +146 -0
  24. package/dist/execution/ssh.d.ts +2 -0
  25. package/dist/execution/ssh.js +17 -0
  26. package/dist/handlers/executor.d.ts +8 -0
  27. package/dist/handlers/executor.js +216 -0
  28. package/dist/healing/claudeHealer.d.ts +17 -0
  29. package/dist/healing/claudeHealer.js +300 -0
  30. package/dist/healing/patchPromoter.d.ts +25 -0
  31. package/dist/healing/patchPromoter.js +118 -0
  32. package/dist/healing/ruleBuilder.d.ts +5 -0
  33. package/dist/healing/ruleBuilder.js +111 -0
  34. package/dist/healing/ruleRepairer.d.ts +8 -0
  35. package/dist/healing/ruleRepairer.js +29 -0
  36. package/dist/healing/ruleValidator.d.ts +22 -0
  37. package/dist/healing/ruleValidator.js +145 -0
  38. package/dist/healing/runHealer.d.ts +11 -0
  39. package/dist/healing/runHealer.js +74 -0
  40. package/dist/index.d.ts +51 -0
  41. package/dist/index.js +62 -0
  42. package/dist/intents/catalog.d.ts +4 -0
  43. package/dist/intents/catalog.js +7 -0
  44. package/dist/nlp/disambiguate.d.ts +2 -0
  45. package/dist/nlp/disambiguate.js +46 -0
  46. package/dist/nlp/fuzzyResolver.d.ts +14 -0
  47. package/dist/nlp/fuzzyResolver.js +108 -0
  48. package/dist/nlp/llmFallback.d.ts +63 -0
  49. package/dist/nlp/llmFallback.js +338 -0
  50. package/dist/nlp/llmParser.d.ts +8 -0
  51. package/dist/nlp/llmParser.js +118 -0
  52. package/dist/nlp/multiClassifier.d.ts +39 -0
  53. package/dist/nlp/multiClassifier.js +181 -0
  54. package/dist/nlp/parseIntent.d.ts +2 -0
  55. package/dist/nlp/parseIntent.js +34 -0
  56. package/dist/nlp/ruleParser.d.ts +2 -0
  57. package/dist/nlp/ruleParser.js +234 -0
  58. package/dist/nlp/semantic.d.ts +104 -0
  59. package/dist/nlp/semantic.js +419 -0
  60. package/dist/nlp/uncertainty.d.ts +42 -0
  61. package/dist/nlp/uncertainty.js +103 -0
  62. package/dist/parsers/apacheParser.d.ts +50 -0
  63. package/dist/parsers/apacheParser.js +152 -0
  64. package/dist/parsers/bindParser.d.ts +40 -0
  65. package/dist/parsers/bindParser.js +189 -0
  66. package/dist/parsers/envFile.d.ts +39 -0
  67. package/dist/parsers/envFile.js +128 -0
  68. package/dist/parsers/fileFinder.d.ts +30 -0
  69. package/dist/parsers/fileFinder.js +226 -0
  70. package/dist/parsers/index.d.ts +27 -0
  71. package/dist/parsers/index.js +193 -0
  72. package/dist/parsers/jsonParser.d.ts +16 -0
  73. package/dist/parsers/jsonParser.js +57 -0
  74. package/dist/parsers/nginxParser.d.ts +47 -0
  75. package/dist/parsers/nginxParser.js +161 -0
  76. package/dist/parsers/passwd.d.ts +25 -0
  77. package/dist/parsers/passwd.js +41 -0
  78. package/dist/parsers/shadow.d.ts +23 -0
  79. package/dist/parsers/shadow.js +50 -0
  80. package/dist/parsers/yamlParser.d.ts +13 -0
  81. package/dist/parsers/yamlParser.js +54 -0
  82. package/dist/policy/confirm.d.ts +2 -0
  83. package/dist/policy/confirm.js +29 -0
  84. package/dist/policy/safety.d.ts +4 -0
  85. package/dist/policy/safety.js +32 -0
  86. package/dist/types/intent.d.ts +205 -0
  87. package/dist/types/intent.js +32 -0
  88. package/dist/types/rules.d.ts +237 -0
  89. package/dist/types/rules.js +50 -0
  90. package/dist/utils/analysis.d.ts +25 -0
  91. package/dist/utils/analysis.js +307 -0
  92. package/dist/utils/autoBackup.d.ts +43 -0
  93. package/dist/utils/autoBackup.js +144 -0
  94. package/dist/utils/config.d.ts +11 -0
  95. package/dist/utils/config.js +32 -0
  96. package/dist/utils/dirAnalysis.d.ts +23 -0
  97. package/dist/utils/dirAnalysis.js +192 -0
  98. package/dist/utils/explain.d.ts +8 -0
  99. package/dist/utils/explain.js +145 -0
  100. package/dist/utils/logger.d.ts +5 -0
  101. package/dist/utils/logger.js +29 -0
  102. package/dist/utils/output.d.ts +2 -0
  103. package/dist/utils/output.js +26 -0
  104. package/dist/utils/paths.d.ts +26 -0
  105. package/dist/utils/paths.js +47 -0
  106. package/dist/utils/permissions.d.ts +64 -0
  107. package/dist/utils/permissions.js +298 -0
  108. package/dist/utils/platform.d.ts +53 -0
  109. package/dist/utils/platform.js +253 -0
  110. package/dist/utils/smartFile.d.ts +29 -0
  111. package/dist/utils/smartFile.js +188 -0
  112. package/dist/utils/spinner.d.ts +53 -0
  113. package/dist/utils/spinner.js +140 -0
  114. package/dist/utils/verbose.d.ts +27 -0
  115. package/dist/utils/verbose.js +131 -0
  116. package/dist/utils/wslPaths.d.ts +31 -0
  117. package/dist/utils/wslPaths.js +145 -0
  118. package/package.json +39 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Auto-backup system.
3
+ *
4
+ * Before any file modification (copy, move, remove, env.set),
5
+ * creates a timestamped backup in ~/.notoken/backups/.
6
+ * Backups are kept for a configurable number of hours (default: 6).
7
+ *
8
+ * Also supports manual rollback.
9
+ */
10
+ import { existsSync, mkdirSync, copyFileSync, unlinkSync, readFileSync } from "node:fs";
11
+ import { resolve, basename } from "node:path";
12
+ import { homedir } from "node:os";
13
+ const BACKUP_ROOT = resolve(homedir(), ".notoken", "backups");
14
+ const DEFAULT_RETENTION_HOURS = 6;
15
+ function ensureBackupDir() {
16
+ if (!existsSync(BACKUP_ROOT)) {
17
+ mkdirSync(BACKUP_ROOT, { recursive: true });
18
+ }
19
+ }
20
+ /**
21
+ * Create a backup of a file before modifying it.
22
+ * Returns the backup record, or null if the file doesn't exist.
23
+ */
24
+ export function createBackup(originalPath, intent, retentionHours = DEFAULT_RETENTION_HOURS) {
25
+ // For remote files, we can't backup locally — this is for local files
26
+ if (!existsSync(originalPath))
27
+ return null;
28
+ ensureBackupDir();
29
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
30
+ const name = basename(originalPath);
31
+ const id = `${name}_${ts}`;
32
+ const backupPath = resolve(BACKUP_ROOT, id);
33
+ copyFileSync(originalPath, backupPath);
34
+ const record = {
35
+ id,
36
+ originalPath,
37
+ backupPath,
38
+ timestamp: new Date().toISOString(),
39
+ intent,
40
+ expiresAt: new Date(Date.now() + retentionHours * 3600_000).toISOString(),
41
+ };
42
+ // Save record index
43
+ const index = loadIndex();
44
+ index.push(record);
45
+ saveIndex(index);
46
+ return record;
47
+ }
48
+ /**
49
+ * Generate the remote backup command to run before modifying a file.
50
+ * This returns a shell command string that creates a backup on the remote server.
51
+ */
52
+ export function getRemoteBackupCommand(filePath) {
53
+ const ts = "$(date +%Y%m%d-%H%M%S)";
54
+ const backupDir = "/tmp/.notoken-backups";
55
+ const name = filePath.split("/").pop() ?? "file";
56
+ return `mkdir -p ${backupDir} && cp -a ${filePath} ${backupDir}/${name}.${ts}.bak 2>/dev/null; `;
57
+ }
58
+ /**
59
+ * Rollback a file from backup.
60
+ */
61
+ export function rollback(id) {
62
+ const index = loadIndex();
63
+ const record = index.find((r) => r.id === id);
64
+ if (!record)
65
+ return false;
66
+ if (!existsSync(record.backupPath))
67
+ return false;
68
+ // Backup the current file before rolling back (safety)
69
+ if (existsSync(record.originalPath)) {
70
+ const safetyBackup = record.backupPath + ".pre-rollback";
71
+ copyFileSync(record.originalPath, safetyBackup);
72
+ }
73
+ copyFileSync(record.backupPath, record.originalPath);
74
+ return true;
75
+ }
76
+ /**
77
+ * List all current backups.
78
+ */
79
+ export function listBackups() {
80
+ const index = loadIndex();
81
+ // Filter out expired
82
+ const now = new Date();
83
+ return index.filter((r) => new Date(r.expiresAt) > now && existsSync(r.backupPath));
84
+ }
85
+ /**
86
+ * Clean up expired backups.
87
+ */
88
+ export function cleanExpiredBackups() {
89
+ const index = loadIndex();
90
+ const now = new Date();
91
+ let cleaned = 0;
92
+ const remaining = index.filter((r) => {
93
+ if (new Date(r.expiresAt) <= now) {
94
+ try {
95
+ if (existsSync(r.backupPath))
96
+ unlinkSync(r.backupPath);
97
+ }
98
+ catch { }
99
+ cleaned++;
100
+ return false;
101
+ }
102
+ return true;
103
+ });
104
+ saveIndex(remaining);
105
+ return cleaned;
106
+ }
107
+ /**
108
+ * Format backup list for display.
109
+ */
110
+ export function formatBackupList(records) {
111
+ const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", yellow: "\x1b[33m", green: "\x1b[32m" };
112
+ if (records.length === 0)
113
+ return `${c.dim}No backups available.${c.reset}`;
114
+ const lines = [`${c.bold}Auto-backups:${c.reset}`];
115
+ for (const r of records) {
116
+ const age = Math.round((Date.now() - new Date(r.timestamp).getTime()) / 60000);
117
+ const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
118
+ const expires = Math.round((new Date(r.expiresAt).getTime() - Date.now()) / 60000);
119
+ const expiresStr = expires < 60 ? `${expires}m left` : `${Math.round(expires / 60)}h left`;
120
+ lines.push(` ${c.cyan}${r.id}${c.reset}`);
121
+ lines.push(` ${r.originalPath} → ${c.dim}${r.backupPath}${c.reset}`);
122
+ lines.push(` ${c.dim}${ageStr} | ${expiresStr} | ${r.intent}${c.reset}`);
123
+ }
124
+ lines.push(`\n ${c.dim}Rollback: :rollback <id>${c.reset}`);
125
+ return lines.join("\n");
126
+ }
127
+ // ─── Index management ────────────────────────────────────────────────────────
128
+ const INDEX_FILE = resolve(BACKUP_ROOT, "index.json");
129
+ function loadIndex() {
130
+ ensureBackupDir();
131
+ if (!existsSync(INDEX_FILE))
132
+ return [];
133
+ try {
134
+ return JSON.parse(readFileSync(INDEX_FILE, "utf-8"));
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ }
140
+ function saveIndex(records) {
141
+ ensureBackupDir();
142
+ const { writeFileSync } = require("node:fs");
143
+ writeFileSync(INDEX_FILE, JSON.stringify(records, null, 2));
144
+ }
@@ -0,0 +1,11 @@
1
+ import { RulesConfig } from "../types/rules.js";
2
+ import { type IntentDef } from "../types/intent.js";
3
+ export declare function loadRules(forceReload?: boolean): RulesConfig;
4
+ export declare function loadIntents(forceReload?: boolean): IntentDef[];
5
+ export declare function getIntentDef(name: string): IntentDef | undefined;
6
+ export declare function loadHosts(): Record<string, {
7
+ host: string;
8
+ description: string;
9
+ }>;
10
+ export declare function getConfigDir(): string;
11
+ export { CONFIG_DIR, DATA_DIR, LOG_DIR, PACKAGE_ROOT, USER_HOME } from "./paths.js";
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { RulesConfig } from "../types/rules.js";
4
+ import { IntentsConfig } from "../types/intent.js";
5
+ import { CONFIG_DIR } from "./paths.js";
6
+ let cachedRules = null;
7
+ let cachedIntents = null;
8
+ export function loadRules(forceReload = false) {
9
+ if (cachedRules && !forceReload)
10
+ return cachedRules;
11
+ const raw = readFileSync(resolve(CONFIG_DIR, "rules.json"), "utf-8");
12
+ cachedRules = RulesConfig.parse(JSON.parse(raw));
13
+ return cachedRules;
14
+ }
15
+ export function loadIntents(forceReload = false) {
16
+ if (cachedIntents && !forceReload)
17
+ return cachedIntents.intents;
18
+ const raw = readFileSync(resolve(CONFIG_DIR, "intents.json"), "utf-8");
19
+ cachedIntents = IntentsConfig.parse(JSON.parse(raw));
20
+ return cachedIntents.intents;
21
+ }
22
+ export function getIntentDef(name) {
23
+ return loadIntents().find((i) => i.name === name);
24
+ }
25
+ export function loadHosts() {
26
+ const raw = readFileSync(resolve(CONFIG_DIR, "hosts.json"), "utf-8");
27
+ return JSON.parse(raw);
28
+ }
29
+ export function getConfigDir() {
30
+ return CONFIG_DIR;
31
+ }
32
+ export { CONFIG_DIR, DATA_DIR, LOG_DIR, PACKAGE_ROOT, USER_HOME } from "./paths.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Directory analysis.
3
+ *
4
+ * When listing files in a directory, detects:
5
+ * - Project type (Node.js, Next.js, Python, PHP, WordPress, Laravel, Go, Rust, etc.)
6
+ * - File type breakdown (code, config, data, logs, images, etc.)
7
+ * - Notable files (README, Dockerfile, CI configs, env files)
8
+ * - Directory size assessment
9
+ */
10
+ export interface ProjectDetection {
11
+ type: string;
12
+ confidence: number;
13
+ indicators: string[];
14
+ }
15
+ export interface FileTypeBreakdown {
16
+ category: string;
17
+ count: number;
18
+ extensions: string[];
19
+ }
20
+ /**
21
+ * Analyze ls -la output and return commentary.
22
+ */
23
+ export declare function analyzeDirectory(output: string): string;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Directory analysis.
3
+ *
4
+ * When listing files in a directory, detects:
5
+ * - Project type (Node.js, Next.js, Python, PHP, WordPress, Laravel, Go, Rust, etc.)
6
+ * - File type breakdown (code, config, data, logs, images, etc.)
7
+ * - Notable files (README, Dockerfile, CI configs, env files)
8
+ * - Directory size assessment
9
+ */
10
+ const c = {
11
+ reset: "\x1b[0m",
12
+ bold: "\x1b[1m",
13
+ dim: "\x1b[2m",
14
+ green: "\x1b[32m",
15
+ yellow: "\x1b[33m",
16
+ red: "\x1b[31m",
17
+ cyan: "\x1b[36m",
18
+ magenta: "\x1b[35m",
19
+ };
20
+ // ─── Project Detection ───────────────────────────────────────────────────────
21
+ const PROJECT_SIGNATURES = [
22
+ { name: "Next.js", files: ["next.config.js", "next.config.ts", "next.config.mjs"], dirs: [".next"], priority: 10 },
23
+ { name: "Nuxt.js", files: ["nuxt.config.ts", "nuxt.config.js"], dirs: [".nuxt"], priority: 10 },
24
+ { name: "SvelteKit", files: ["svelte.config.js"], dirs: [], priority: 10 },
25
+ { name: "Remix", files: ["remix.config.js"], dirs: [], priority: 10 },
26
+ { name: "Astro", files: ["astro.config.mjs"], dirs: [], priority: 10 },
27
+ { name: "React (CRA)", files: ["react-scripts"], dirs: [], priority: 8 },
28
+ { name: "Vue.js", files: ["vue.config.js"], dirs: [], priority: 8 },
29
+ { name: "Angular", files: ["angular.json"], dirs: [], priority: 8 },
30
+ { name: "Node.js", files: ["package.json"], dirs: ["node_modules"], priority: 5 },
31
+ { name: "TypeScript", files: ["tsconfig.json"], dirs: [], priority: 4 },
32
+ { name: "Deno", files: ["deno.json", "deno.jsonc"], dirs: [], priority: 8 },
33
+ { name: "Bun", files: ["bunfig.toml"], dirs: [], priority: 8 },
34
+ { name: "Python", files: ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"], dirs: ["__pycache__", "venv", ".venv"], priority: 5 },
35
+ { name: "Django", files: ["manage.py"], dirs: [], priority: 8 },
36
+ { name: "Flask", files: ["app.py", "wsgi.py"], dirs: [], priority: 6 },
37
+ { name: "FastAPI", files: ["main.py"], dirs: [], priority: 5 },
38
+ { name: "PHP", files: ["composer.json", "index.php"], dirs: ["vendor"], priority: 5 },
39
+ { name: "WordPress", files: ["wp-config.php", "wp-login.php"], dirs: ["wp-content", "wp-admin", "wp-includes"], priority: 10 },
40
+ { name: "Laravel", files: ["artisan"], dirs: ["app", "bootstrap", "routes"], priority: 9 },
41
+ { name: "Symfony", files: ["symfony.lock"], dirs: ["src", "config"], priority: 8 },
42
+ { name: "Go", files: ["go.mod", "go.sum"], dirs: [], priority: 7 },
43
+ { name: "Rust", files: ["Cargo.toml", "Cargo.lock"], dirs: ["target"], priority: 7 },
44
+ { name: "Java/Maven", files: ["pom.xml"], dirs: ["src/main"], priority: 7 },
45
+ { name: "Java/Gradle", files: ["build.gradle", "build.gradle.kts"], dirs: [], priority: 7 },
46
+ { name: "Ruby on Rails", files: ["Gemfile", "Rakefile"], dirs: ["app", "config", "db"], priority: 7 },
47
+ { name: "Ruby", files: ["Gemfile"], dirs: [], priority: 4 },
48
+ { name: ".NET/C#", files: ["*.csproj", "*.sln"], dirs: ["bin", "obj"], priority: 7 },
49
+ { name: "Docker project", files: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"], dirs: [], priority: 3 },
50
+ { name: "Terraform", files: ["main.tf", "terraform.tfstate"], dirs: [".terraform"], priority: 8 },
51
+ { name: "Ansible", files: ["ansible.cfg", "playbook.yml"], dirs: ["roles"], priority: 8 },
52
+ { name: "Shell scripts", files: ["*.sh"], dirs: [], priority: 2 },
53
+ { name: "Batch scripts", files: ["*.bat", "*.cmd", "*.ps1"], dirs: [], priority: 2 },
54
+ ];
55
+ // ─── File Categories ─────────────────────────────────────────────────────────
56
+ const FILE_CATEGORIES = {
57
+ "Code": [".js", ".ts", ".jsx", ".tsx", ".py", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".cs", ".php", ".swift", ".kt"],
58
+ "Config": [".json", ".yml", ".yaml", ".toml", ".ini", ".cfg", ".conf", ".env", ".xml"],
59
+ "Web": [".html", ".htm", ".css", ".scss", ".sass", ".less", ".svg"],
60
+ "Data": [".csv", ".sql", ".db", ".sqlite", ".parquet"],
61
+ "Documents": [".md", ".txt", ".doc", ".docx", ".pdf", ".rtf"],
62
+ "Images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".ico", ".bmp", ".tiff"],
63
+ "Archives": [".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z"],
64
+ "Logs": [".log"],
65
+ "Scripts": [".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1"],
66
+ "Build": [".o", ".class", ".pyc", ".wasm"],
67
+ };
68
+ const NOTABLE_FILES = new Set([
69
+ "README.md", "README", "LICENSE", "CHANGELOG.md",
70
+ "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
71
+ ".env", ".env.local", ".env.production",
72
+ ".gitignore", ".dockerignore", ".editorconfig",
73
+ "Makefile", "Procfile", "Vagrantfile",
74
+ ".github", ".gitlab-ci.yml", "Jenkinsfile",
75
+ "package.json", "tsconfig.json", "requirements.txt",
76
+ ]);
77
+ /**
78
+ * Analyze ls -la output and return commentary.
79
+ */
80
+ export function analyzeDirectory(output) {
81
+ const lines = [];
82
+ const entries = parseLsOutput(output);
83
+ if (entries.length === 0)
84
+ return "";
85
+ const files = entries.filter((e) => e.type === "file");
86
+ const dirs = entries.filter((e) => e.type === "directory");
87
+ const fileNames = files.map((e) => e.name);
88
+ const dirNames = dirs.map((e) => e.name);
89
+ const allNames = [...fileNames, ...dirNames];
90
+ lines.push(`\n${c.bold}${c.cyan}── Analysis ──${c.reset}`);
91
+ lines.push(` ${files.length} file(s), ${dirs.length} directory(ies)`);
92
+ // Detect project type
93
+ const detected = detectProjects(fileNames, dirNames);
94
+ if (detected.length > 0) {
95
+ lines.push(`\n ${c.bold}Project detected:${c.reset}`);
96
+ for (const d of detected) {
97
+ lines.push(` ${c.magenta}${d.type}${c.reset} — ${d.indicators.join(", ")}`);
98
+ }
99
+ }
100
+ // File type breakdown
101
+ const breakdown = categorizeFiles(fileNames);
102
+ if (breakdown.length > 0) {
103
+ lines.push(`\n ${c.bold}File types:${c.reset}`);
104
+ for (const b of breakdown.slice(0, 6)) {
105
+ const bar = "█".repeat(Math.min(20, Math.round((b.count / files.length) * 20)));
106
+ lines.push(` ${c.dim}${bar}${c.reset} ${b.category}: ${b.count} (${b.extensions.join(", ")})`);
107
+ }
108
+ }
109
+ // Notable files
110
+ const notable = allNames.filter((n) => NOTABLE_FILES.has(n));
111
+ if (notable.length > 0) {
112
+ lines.push(`\n ${c.bold}Notable:${c.reset} ${notable.join(", ")}`);
113
+ }
114
+ // Warnings
115
+ if (dirNames.includes("node_modules")) {
116
+ lines.push(` ${c.yellow}⚠ node_modules present — may be large${c.reset}`);
117
+ }
118
+ if (fileNames.some((f) => f === ".env" || f.startsWith(".env."))) {
119
+ lines.push(` ${c.yellow}⚠ .env file(s) present — check they're in .gitignore${c.reset}`);
120
+ }
121
+ return lines.join("\n");
122
+ }
123
+ function detectProjects(files, dirs) {
124
+ const results = [];
125
+ for (const sig of PROJECT_SIGNATURES) {
126
+ const matchedFiles = sig.files.filter((f) => {
127
+ if (f.startsWith("*")) {
128
+ const ext = f.slice(1);
129
+ return files.some((name) => name.endsWith(ext));
130
+ }
131
+ return files.includes(f);
132
+ });
133
+ const matchedDirs = sig.dirs.filter((d) => dirs.includes(d));
134
+ if (matchedFiles.length > 0 || matchedDirs.length > 0) {
135
+ results.push({
136
+ type: sig.name,
137
+ confidence: sig.priority / 10,
138
+ indicators: [...matchedFiles, ...matchedDirs.map((d) => `${d}/`)],
139
+ });
140
+ }
141
+ }
142
+ // Sort by priority, take top 3
143
+ results.sort((a, b) => b.confidence - a.confidence);
144
+ return results.slice(0, 3);
145
+ }
146
+ function categorizeFiles(files) {
147
+ const counts = new Map();
148
+ for (const file of files) {
149
+ const dot = file.lastIndexOf(".");
150
+ const ext = dot >= 0 ? file.slice(dot).toLowerCase() : "";
151
+ let category = "Other";
152
+ for (const [cat, exts] of Object.entries(FILE_CATEGORIES)) {
153
+ if (exts.includes(ext)) {
154
+ category = cat;
155
+ break;
156
+ }
157
+ }
158
+ const entry = counts.get(category) ?? { count: 0, extensions: new Set() };
159
+ entry.count++;
160
+ if (ext)
161
+ entry.extensions.add(ext);
162
+ counts.set(category, entry);
163
+ }
164
+ return Array.from(counts.entries())
165
+ .map(([category, { count, extensions }]) => ({
166
+ category,
167
+ count,
168
+ extensions: Array.from(extensions).slice(0, 5),
169
+ }))
170
+ .sort((a, b) => b.count - a.count);
171
+ }
172
+ function parseLsOutput(output) {
173
+ const entries = [];
174
+ for (const line of output.split("\n")) {
175
+ // Match: drwxr-xr-x 2 root root 4096 Jan 1 12:00 dirname
176
+ // Match: -rw-r--r-- 1 root root 1234 Jan 1 12:00 filename
177
+ const match = line.match(/^([dlcbsp-])([rwxsStT-]{9})\s+\d+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+\d+\s+[\d:]+\s+(.+)$/);
178
+ if (!match)
179
+ continue;
180
+ const typeChar = match[1];
181
+ const name = match[4].trim();
182
+ if (name === "." || name === "..")
183
+ continue;
184
+ entries.push({
185
+ type: typeChar === "d" ? "directory" : typeChar === "l" ? "symlink" : typeChar === "-" ? "file" : "other",
186
+ permissions: match[1] + match[2],
187
+ name,
188
+ size: match[3],
189
+ });
190
+ }
191
+ return entries;
192
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Explain mode.
3
+ *
4
+ * Shows WHY a command was chosen, what alternatives exist,
5
+ * what could go wrong, and the full decision chain.
6
+ */
7
+ import type { ParsedCommand } from "../types/intent.js";
8
+ export declare function formatExplain(parsed: ParsedCommand, rawText: string): string;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Explain mode.
3
+ *
4
+ * Shows WHY a command was chosen, what alternatives exist,
5
+ * what could go wrong, and the full decision chain.
6
+ */
7
+ import { getIntentDef } from "./config.js";
8
+ import { classifyMulti } from "../nlp/multiClassifier.js";
9
+ import { detectLocalPlatform } from "./platform.js";
10
+ const c = {
11
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
12
+ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", magenta: "\x1b[35m",
13
+ };
14
+ export function formatExplain(parsed, rawText) {
15
+ const { intent } = parsed;
16
+ const def = getIntentDef(intent.intent);
17
+ const lines = [];
18
+ lines.push(`${c.bold}${c.magenta}── Explain ──${c.reset}\n`);
19
+ // 1. What was understood
20
+ lines.push(`${c.bold}What I understood:${c.reset}`);
21
+ lines.push(` Input: "${rawText}"`);
22
+ lines.push(` Intent: ${c.cyan}${intent.intent}${c.reset} (${(intent.confidence * 100).toFixed(0)}% confidence)`);
23
+ if (def) {
24
+ lines.push(` Description: ${def.description}`);
25
+ }
26
+ // 2. Why this intent was chosen
27
+ lines.push(`\n${c.bold}Why this intent:${c.reset}`);
28
+ const multi = classifyMulti(rawText);
29
+ if (multi.votes.length > 0) {
30
+ // Group by classifier
31
+ const byClassifier = new Map();
32
+ for (const v of multi.votes) {
33
+ const list = byClassifier.get(v.classifier) ?? [];
34
+ list.push(v);
35
+ byClassifier.set(v.classifier, list);
36
+ }
37
+ for (const [classifier, votes] of byClassifier) {
38
+ const top = votes.sort((a, b) => b.confidence - a.confidence)[0];
39
+ const weight = { synonym: "1.0x", semantic: "0.8x", context: "0.6x", fuzzy: "0.5x" }[classifier] ?? "?";
40
+ lines.push(` ${c.cyan}${classifier}${c.reset} (${weight}): ${top.intent} — ${top.reason}`);
41
+ }
42
+ }
43
+ if (multi.ambiguous) {
44
+ lines.push(` ${c.yellow}⚠ Ambiguous — top intents scored similarly. May benefit from rephrasing.${c.reset}`);
45
+ }
46
+ // 3. Alternatives considered
47
+ if (multi.scores.length > 1) {
48
+ lines.push(`\n${c.bold}Alternatives considered:${c.reset}`);
49
+ for (const s of multi.scores.slice(1, 4)) {
50
+ const altDef = getIntentDef(s.intent);
51
+ lines.push(` ${c.dim}${s.intent}${c.reset} (${(s.score * 100).toFixed(0)}%) — ${altDef?.description ?? "unknown"}`);
52
+ }
53
+ }
54
+ // 4. Fields extracted
55
+ const fields = Object.entries(intent.fields).filter(([, v]) => v !== undefined && v !== "");
56
+ if (fields.length > 0) {
57
+ lines.push(`\n${c.bold}Fields extracted:${c.reset}`);
58
+ for (const [key, value] of fields) {
59
+ const fieldDef = def?.fields[key];
60
+ const src = wasExplicit(rawText, String(value)) ? "from input" : "default";
61
+ lines.push(` ${key}: ${c.bold}${value}${c.reset} ${c.dim}(${fieldDef?.type ?? "?"}, ${src})${c.reset}`);
62
+ }
63
+ }
64
+ if (parsed.missingFields.length > 0) {
65
+ lines.push(` ${c.yellow}Missing: ${parsed.missingFields.join(", ")}${c.reset}`);
66
+ }
67
+ // 5. What will happen
68
+ if (def) {
69
+ lines.push(`\n${c.bold}What will happen:${c.reset}`);
70
+ lines.push(` Execution: ${def.execution === "local" ? "runs locally" : "runs via SSH"}`);
71
+ lines.push(` Risk: ${formatRisk(def.riskLevel)}`);
72
+ lines.push(` Confirmation: ${def.requiresConfirmation ? "yes (will ask)" : "no"}`);
73
+ // Command preview
74
+ let cmd = def.command;
75
+ for (const [k, v] of fields) {
76
+ if (v !== undefined)
77
+ cmd = cmd.replaceAll(`{{${k}}}`, String(v));
78
+ }
79
+ cmd = cmd.replace(/\{\{[a-zA-Z_]+\}\}/g, "").trim();
80
+ if (cmd.length <= 120) {
81
+ lines.push(` Command: ${c.dim}${cmd}${c.reset}`);
82
+ }
83
+ else {
84
+ lines.push(` Command: ${c.dim}${cmd.slice(0, 117)}...${c.reset}`);
85
+ }
86
+ if (def.allowlist && def.allowlist.length > 0) {
87
+ lines.push(` Allowlist: ${def.allowlist.join(", ")}`);
88
+ }
89
+ }
90
+ // 6. What could go wrong
91
+ lines.push(`\n${c.bold}What could go wrong:${c.reset}`);
92
+ const risks = getKnownRisks(intent.intent, intent.fields);
93
+ if (risks.length > 0) {
94
+ for (const risk of risks) {
95
+ lines.push(` ${c.yellow}⚠${c.reset} ${risk}`);
96
+ }
97
+ }
98
+ else {
99
+ lines.push(` ${c.green}Low risk — read-only or safe operation.${c.reset}`);
100
+ }
101
+ // 7. Platform context
102
+ const platform = detectLocalPlatform();
103
+ lines.push(`\n${c.bold}Platform:${c.reset}`);
104
+ lines.push(` ${platform.distro}${platform.isWSL ? " (WSL)" : ""} | ${platform.packageManager} | ${platform.initSystem}`);
105
+ return lines.join("\n");
106
+ }
107
+ function wasExplicit(rawText, value) {
108
+ return rawText.toLowerCase().includes(value.toLowerCase());
109
+ }
110
+ function formatRisk(level) {
111
+ if (level === "high")
112
+ return `${c.red}HIGH${c.reset} — destructive, requires confirmation`;
113
+ if (level === "medium")
114
+ return `${c.yellow}MEDIUM${c.reset} — modifies state`;
115
+ return `${c.green}LOW${c.reset} — read-only or safe`;
116
+ }
117
+ function getKnownRisks(intentName, fields) {
118
+ const risks = [];
119
+ const env = fields.environment;
120
+ if (env === "prod") {
121
+ risks.push("Running on PRODUCTION — ensure this is intentional.");
122
+ }
123
+ if (intentName.includes("restart") || intentName.includes("stop")) {
124
+ risks.push("Service will be temporarily unavailable during restart.");
125
+ }
126
+ if (intentName.includes("remove") || intentName.includes("delete") || intentName.includes("prune")) {
127
+ risks.push("Data will be permanently deleted. Auto-backup runs first if enabled.");
128
+ }
129
+ if (intentName.includes("deploy")) {
130
+ risks.push("Deployment may affect live users. Rollback available if it fails.");
131
+ }
132
+ if (intentName.includes("chmod") || intentName.includes("chown")) {
133
+ risks.push("Permission changes can lock you out if applied incorrectly.");
134
+ }
135
+ if (intentName.includes("reboot") || intentName.includes("shutdown")) {
136
+ risks.push("Server will be unreachable during reboot. Ensure you have console access.");
137
+ }
138
+ if (intentName === "docker.prune") {
139
+ risks.push("Removes ALL unused containers, images, and volumes. Data in unnamed volumes is lost.");
140
+ }
141
+ if (intentName === "git.reset") {
142
+ risks.push("May discard uncommitted changes. Ensure work is saved.");
143
+ }
144
+ return risks;
145
+ }
@@ -0,0 +1,5 @@
1
+ import type { FailureLog } from "../types/rules.js";
2
+ export declare function logFailure(entry: FailureLog): void;
3
+ export declare function loadFailures(): FailureLog[];
4
+ export declare function clearFailures(): void;
5
+ export declare function log(level: "info" | "warn" | "error", message: string): void;
@@ -0,0 +1,29 @@
1
+ import { writeFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { LOG_DIR } from "./paths.js";
4
+ const FAILURE_LOG = resolve(LOG_DIR, "failures.json");
5
+ function ensureLogDir() {
6
+ if (!existsSync(LOG_DIR)) {
7
+ mkdirSync(LOG_DIR, { recursive: true });
8
+ }
9
+ }
10
+ export function logFailure(entry) {
11
+ ensureLogDir();
12
+ const existing = loadFailures();
13
+ existing.push(entry);
14
+ writeFileSync(FAILURE_LOG, JSON.stringify(existing, null, 2));
15
+ }
16
+ export function loadFailures() {
17
+ if (!existsSync(FAILURE_LOG))
18
+ return [];
19
+ const raw = readFileSync(FAILURE_LOG, "utf-8");
20
+ return JSON.parse(raw);
21
+ }
22
+ export function clearFailures() {
23
+ ensureLogDir();
24
+ writeFileSync(FAILURE_LOG, "[]");
25
+ }
26
+ export function log(level, message) {
27
+ const prefix = level === "error" ? "ERROR" : level === "warn" ? "WARN" : "INFO";
28
+ console.error(`[${prefix}] ${message}`);
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { ParsedCommand } from "../types/intent.js";
2
+ export declare function formatParsedCommand(cmd: ParsedCommand): string;
@@ -0,0 +1,26 @@
1
+ export function formatParsedCommand(cmd) {
2
+ const lines = [];
3
+ const { intent } = cmd;
4
+ lines.push(`Intent: ${intent.intent}`);
5
+ lines.push(`Confidence: ${(intent.confidence * 100).toFixed(0)}%`);
6
+ const entries = Object.entries(intent.fields).filter(([, v]) => v !== undefined);
7
+ if (entries.length > 0) {
8
+ lines.push("Fields:");
9
+ for (const [k, v] of entries) {
10
+ lines.push(` ${k}: ${v}`);
11
+ }
12
+ }
13
+ if (cmd.missingFields.length > 0) {
14
+ lines.push(`Missing: ${cmd.missingFields.join(", ")}`);
15
+ }
16
+ if (cmd.ambiguousFields.length > 0) {
17
+ lines.push("Ambiguous:");
18
+ for (const a of cmd.ambiguousFields) {
19
+ lines.push(` ${a.field}: ${a.candidates.join(" | ")}`);
20
+ }
21
+ }
22
+ if (cmd.needsClarification) {
23
+ lines.push("=> Clarification needed before execution.");
24
+ }
25
+ return lines.join("\n");
26
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Centralized path resolution.
3
+ *
4
+ * Single source of truth for all directory paths used by mycli.
5
+ * Supports three modes:
6
+ * 1. Development (tsx): src/utils/paths.ts → resolve("../..")
7
+ * 2. npm package: dist/utils/paths.js → resolve("../..")
8
+ * 3. SEA binary: embedded assets, writable dirs in ~/.mycli/
9
+ *
10
+ * Writable directories (data, logs) always go to ~/.mycli/ so they
11
+ * work in all modes. Config is read-only and ships with the package.
12
+ */
13
+ /** Whether running as a Node.js Single Executable Application */
14
+ export declare function isSEA(): boolean;
15
+ /** Package root: two levels up from dist/utils/ or src/utils/ */
16
+ export declare const PACKAGE_ROOT: string;
17
+ /** Read-only config directory (ships with the package) */
18
+ export declare const CONFIG_DIR: string;
19
+ /** User data root — writable, lives in home directory */
20
+ export declare const USER_HOME: string;
21
+ /** Writable data directory (history, sessions) */
22
+ export declare const DATA_DIR: string;
23
+ /** Writable logs directory (failures, uncertainty) */
24
+ export declare const LOG_DIR: string;
25
+ /** Ensure writable directories exist */
26
+ export declare function ensureUserDirs(): void;