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,43 @@
1
+ export interface RedactionResult {
2
+ /** Text with secrets replaced by placeholders */
3
+ redactedText: string;
4
+ /** Number of secrets found */
5
+ secretCount: number;
6
+ /** The placeholder IDs created */
7
+ placeholders: Array<{
8
+ id: string;
9
+ label: string;
10
+ }>;
11
+ }
12
+ /**
13
+ * Scan text for secrets and replace with placeholders.
14
+ * Secrets are stored in memory only.
15
+ */
16
+ export declare function redactSecrets(text: string): RedactionResult;
17
+ /**
18
+ * Retrieve a secret from memory by placeholder ID.
19
+ */
20
+ export declare function getSecret(placeholderId: string): string | undefined;
21
+ /**
22
+ * List all secret placeholder IDs in memory.
23
+ */
24
+ export declare function listSecrets(): Array<{
25
+ id: string;
26
+ preview: string;
27
+ }>;
28
+ /**
29
+ * Save secrets to a file (only when user explicitly asks).
30
+ */
31
+ export declare function saveSecretsToFile(filepath?: string): string;
32
+ /**
33
+ * Load secrets from a previously saved file back into memory.
34
+ */
35
+ export declare function loadSecretsFromFile(filepath: string): number;
36
+ /**
37
+ * Clear all secrets from memory.
38
+ */
39
+ export declare function clearSecrets(): void;
40
+ /**
41
+ * Resolve placeholders back to real values (for execution only, not storage).
42
+ */
43
+ export declare function resolvePlaceholders(text: string): string;
@@ -0,0 +1,129 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { resolve, dirname } from "node:path";
4
+ import { DATA_DIR } from "../utils/paths.js";
5
+ /**
6
+ * Secret manager.
7
+ *
8
+ * Detects passwords, tokens, and secrets in conversation text.
9
+ * Replaces them with <password.UUID> placeholders in stored conversations.
10
+ * Secrets live only in memory unless the user explicitly saves them.
11
+ */
12
+ // In-memory secret store (never persisted unless user asks)
13
+ const secretStore = new Map();
14
+ // Patterns that look like secrets
15
+ const SECRET_PATTERNS = [
16
+ // Explicit password flags
17
+ { pattern: /(?:--password|--pass|-p)\s+(\S+)/i, label: "password" },
18
+ { pattern: /(?:password|passwd|pass)\s*[=:]\s*(\S+)/i, label: "password" },
19
+ // API keys / tokens
20
+ { pattern: /(?:api[_-]?key|token|secret|auth)\s*[=:]\s*(\S+)/i, label: "api_key" },
21
+ { pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/, label: "api_key" },
22
+ { pattern: /\b(ghp_[a-zA-Z0-9]{36,})\b/, label: "github_token" },
23
+ { pattern: /\b(glpat-[a-zA-Z0-9\-_]{20,})\b/, label: "gitlab_token" },
24
+ // SSH private key content
25
+ { pattern: /(-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA )?PRIVATE KEY-----)/, label: "private_key" },
26
+ // Generic high-entropy strings that look like secrets (32+ chars, mixed case + digits)
27
+ { pattern: /\b([A-Za-z0-9+\/]{32,}={0,2})\b/, label: "possible_secret" },
28
+ // Connection strings with passwords
29
+ { pattern: /:\/\/([^:]+):([^@]+)@/i, label: "connection_password" },
30
+ ];
31
+ /**
32
+ * Scan text for secrets and replace with placeholders.
33
+ * Secrets are stored in memory only.
34
+ */
35
+ export function redactSecrets(text) {
36
+ let redacted = text;
37
+ const placeholders = [];
38
+ for (const { pattern, label } of SECRET_PATTERNS) {
39
+ // Skip the generic high-entropy pattern for short texts
40
+ if (label === "possible_secret" && text.length < 50)
41
+ continue;
42
+ const match = redacted.match(pattern);
43
+ if (!match)
44
+ continue;
45
+ // For connection strings, redact just the password part (group 2)
46
+ const secretValue = match[2] ?? match[1];
47
+ if (!secretValue || secretValue.length < 6)
48
+ continue;
49
+ // Don't redact common words that happen to match
50
+ if (isCommonWord(secretValue))
51
+ continue;
52
+ const id = `<${label}.${randomUUID().slice(0, 8)}>`;
53
+ secretStore.set(id, secretValue);
54
+ redacted = redacted.replace(secretValue, id);
55
+ placeholders.push({ id, label });
56
+ }
57
+ return {
58
+ redactedText: redacted,
59
+ secretCount: placeholders.length,
60
+ placeholders,
61
+ };
62
+ }
63
+ /**
64
+ * Retrieve a secret from memory by placeholder ID.
65
+ */
66
+ export function getSecret(placeholderId) {
67
+ return secretStore.get(placeholderId);
68
+ }
69
+ /**
70
+ * List all secret placeholder IDs in memory.
71
+ */
72
+ export function listSecrets() {
73
+ return Array.from(secretStore.entries()).map(([id, value]) => ({
74
+ id,
75
+ preview: value.slice(0, 4) + "****",
76
+ }));
77
+ }
78
+ /**
79
+ * Save secrets to a file (only when user explicitly asks).
80
+ */
81
+ export function saveSecretsToFile(filepath) {
82
+ const file = filepath ?? resolve(DATA_DIR, `secrets_${Date.now()}.json`);
83
+ const dir = dirname(file);
84
+ if (!existsSync(dir))
85
+ mkdirSync(dir, { recursive: true });
86
+ const data = {};
87
+ for (const [id, value] of secretStore.entries()) {
88
+ data[id] = value;
89
+ }
90
+ writeFileSync(file, JSON.stringify(data, null, 2), { mode: 0o600 });
91
+ return file;
92
+ }
93
+ /**
94
+ * Load secrets from a previously saved file back into memory.
95
+ */
96
+ export function loadSecretsFromFile(filepath) {
97
+ if (!existsSync(filepath))
98
+ return 0;
99
+ const data = JSON.parse(readFileSync(filepath, "utf-8"));
100
+ let count = 0;
101
+ for (const [id, value] of Object.entries(data)) {
102
+ secretStore.set(id, value);
103
+ count++;
104
+ }
105
+ return count;
106
+ }
107
+ /**
108
+ * Clear all secrets from memory.
109
+ */
110
+ export function clearSecrets() {
111
+ secretStore.clear();
112
+ }
113
+ /**
114
+ * Resolve placeholders back to real values (for execution only, not storage).
115
+ */
116
+ export function resolvePlaceholders(text) {
117
+ let resolved = text;
118
+ for (const [id, value] of secretStore.entries()) {
119
+ resolved = resolved.replaceAll(id, value);
120
+ }
121
+ return resolved;
122
+ }
123
+ function isCommonWord(value) {
124
+ const common = new Set([
125
+ "password", "secret", "token", "admin", "root", "localhost",
126
+ "default", "staging", "production", "develop", "master", "main",
127
+ ]);
128
+ return common.has(value.toLowerCase());
129
+ }
@@ -0,0 +1,94 @@
1
+ export interface ConversationTurn {
2
+ id: number;
3
+ timestamp: string;
4
+ role: "user" | "system";
5
+ rawText: string;
6
+ intent?: string;
7
+ confidence?: number;
8
+ fields?: Record<string, unknown>;
9
+ result?: string;
10
+ error?: string;
11
+ /** Entities mentioned in this turn */
12
+ entities: ConversationEntity[];
13
+ /** Uncertainty info for this turn */
14
+ uncertainty?: UncertaintyReport;
15
+ }
16
+ export interface ConversationEntity {
17
+ text: string;
18
+ type: "service" | "environment" | "path" | "user" | "branch" | "container" | "unknown";
19
+ resolved?: string;
20
+ }
21
+ export interface UncertaintyReport {
22
+ unknownTokens: string[];
23
+ lowConfidenceFields: Array<{
24
+ field: string;
25
+ value: string;
26
+ confidence: number;
27
+ }>;
28
+ overallConfidence: number;
29
+ }
30
+ export interface Conversation {
31
+ id: string;
32
+ folderPath: string;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ turns: ConversationTurn[];
36
+ /** Running knowledge of entities mentioned across turns */
37
+ knowledgeTree: KnowledgeNode[];
38
+ }
39
+ export interface KnowledgeNode {
40
+ entity: string;
41
+ type: ConversationEntity["type"];
42
+ firstMentioned: number;
43
+ lastMentioned: number;
44
+ /** How many turns reference this entity */
45
+ frequency: number;
46
+ /** Related entities seen in the same turns */
47
+ coOccurrences: string[];
48
+ /** Most recent field role (service, environment, target, etc.) */
49
+ lastRole?: string;
50
+ }
51
+ /**
52
+ * Create a new conversation.
53
+ */
54
+ export declare function createConversation(folderPath: string): Conversation;
55
+ /**
56
+ * Save a conversation to disk.
57
+ */
58
+ export declare function saveConversation(conv: Conversation): void;
59
+ /**
60
+ * Load a conversation by ID.
61
+ */
62
+ export declare function loadConversation(folderPath: string, conversationId: string): Conversation | null;
63
+ /**
64
+ * Load the most recent conversation for a folder path, or create a new one.
65
+ */
66
+ export declare function getOrCreateConversation(folderPath: string): Conversation;
67
+ /**
68
+ * List all conversations for a folder path.
69
+ */
70
+ export declare function listConversations(folderPath: string): Array<{
71
+ id: string;
72
+ createdAt: string;
73
+ turns: number;
74
+ }>;
75
+ /**
76
+ * Add a user turn to the conversation.
77
+ */
78
+ export declare function addUserTurn(conv: Conversation, rawText: string, intent?: string, confidence?: number, fields?: Record<string, unknown>, entities?: ConversationEntity[], uncertainty?: UncertaintyReport): ConversationTurn;
79
+ /**
80
+ * Add a system result turn.
81
+ */
82
+ export declare function addSystemTurn(conv: Conversation, rawText: string, result?: string, error?: string): ConversationTurn;
83
+ /**
84
+ * Get the most recently mentioned entity of a given type.
85
+ */
86
+ export declare function getLastEntity(conv: Conversation, type: ConversationEntity["type"]): KnowledgeNode | undefined;
87
+ /**
88
+ * Get all entities, sorted by recency.
89
+ */
90
+ export declare function getRecentEntities(conv: Conversation, limit?: number): KnowledgeNode[];
91
+ /**
92
+ * Get the last N user turns.
93
+ */
94
+ export declare function getRecentTurns(conv: Conversation, count?: number): ConversationTurn[];
@@ -0,0 +1,184 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CONVERSATIONS_ROOT = resolve(homedir(), ".notoken", "conversations");
5
+ // ─── Store ───────────────────────────────────────────────────────────────────
6
+ function getConversationDir(folderPath) {
7
+ // Sanitize the folder path for filesystem use
8
+ const safePath = folderPath.replace(/[^a-zA-Z0-9_\-\/]/g, "_").replace(/^\/+/, "");
9
+ const dir = resolve(CONVERSATIONS_ROOT, safePath || "default");
10
+ if (!existsSync(dir)) {
11
+ mkdirSync(dir, { recursive: true });
12
+ }
13
+ return dir;
14
+ }
15
+ function getConversationFile(folderPath, conversationId) {
16
+ return resolve(getConversationDir(folderPath), `${conversationId}.json`);
17
+ }
18
+ /**
19
+ * Create a new conversation.
20
+ */
21
+ export function createConversation(folderPath) {
22
+ const id = `conv_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
23
+ const conv = {
24
+ id,
25
+ folderPath,
26
+ createdAt: new Date().toISOString(),
27
+ updatedAt: new Date().toISOString(),
28
+ turns: [],
29
+ knowledgeTree: [],
30
+ };
31
+ saveConversation(conv);
32
+ return conv;
33
+ }
34
+ /**
35
+ * Save a conversation to disk.
36
+ */
37
+ export function saveConversation(conv) {
38
+ const file = getConversationFile(conv.folderPath, conv.id);
39
+ conv.updatedAt = new Date().toISOString();
40
+ writeFileSync(file, JSON.stringify(conv, null, 2));
41
+ }
42
+ /**
43
+ * Load a conversation by ID.
44
+ */
45
+ export function loadConversation(folderPath, conversationId) {
46
+ const file = getConversationFile(folderPath, conversationId);
47
+ if (!existsSync(file))
48
+ return null;
49
+ return JSON.parse(readFileSync(file, "utf-8"));
50
+ }
51
+ /**
52
+ * Load the most recent conversation for a folder path, or create a new one.
53
+ */
54
+ export function getOrCreateConversation(folderPath) {
55
+ const dir = getConversationDir(folderPath);
56
+ const files = readdirSync(dir)
57
+ .filter((f) => f.endsWith(".json"))
58
+ .sort()
59
+ .reverse();
60
+ if (files.length > 0) {
61
+ const latest = JSON.parse(readFileSync(resolve(dir, files[0]), "utf-8"));
62
+ // If last activity was within 1 hour, continue it
63
+ const age = Date.now() - new Date(latest.updatedAt).getTime();
64
+ if (age < 3600_000)
65
+ return latest;
66
+ }
67
+ return createConversation(folderPath);
68
+ }
69
+ /**
70
+ * List all conversations for a folder path.
71
+ */
72
+ export function listConversations(folderPath) {
73
+ const dir = getConversationDir(folderPath);
74
+ if (!existsSync(dir))
75
+ return [];
76
+ return readdirSync(dir)
77
+ .filter((f) => f.endsWith(".json"))
78
+ .map((f) => {
79
+ const conv = JSON.parse(readFileSync(resolve(dir, f), "utf-8"));
80
+ return { id: conv.id, createdAt: conv.createdAt, turns: conv.turns.length };
81
+ })
82
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
83
+ }
84
+ // ─── Turn Management ─────────────────────────────────────────────────────────
85
+ /**
86
+ * Add a user turn to the conversation.
87
+ */
88
+ export function addUserTurn(conv, rawText, intent, confidence, fields, entities, uncertainty) {
89
+ const turn = {
90
+ id: conv.turns.length + 1,
91
+ timestamp: new Date().toISOString(),
92
+ role: "user",
93
+ rawText,
94
+ intent,
95
+ confidence,
96
+ fields,
97
+ entities: entities ?? [],
98
+ uncertainty,
99
+ };
100
+ conv.turns.push(turn);
101
+ // Update knowledge tree
102
+ for (const entity of turn.entities) {
103
+ updateKnowledge(conv, entity, turn.id, fields);
104
+ }
105
+ saveConversation(conv);
106
+ return turn;
107
+ }
108
+ /**
109
+ * Add a system result turn.
110
+ */
111
+ export function addSystemTurn(conv, rawText, result, error) {
112
+ const turn = {
113
+ id: conv.turns.length + 1,
114
+ timestamp: new Date().toISOString(),
115
+ role: "system",
116
+ rawText,
117
+ result,
118
+ error,
119
+ entities: [],
120
+ };
121
+ conv.turns.push(turn);
122
+ saveConversation(conv);
123
+ return turn;
124
+ }
125
+ // ─── Knowledge Tree ──────────────────────────────────────────────────────────
126
+ function updateKnowledge(conv, entity, turnId, fields) {
127
+ const key = entity.resolved ?? entity.text;
128
+ let node = conv.knowledgeTree.find((n) => n.entity === key);
129
+ if (!node) {
130
+ node = {
131
+ entity: key,
132
+ type: entity.type,
133
+ firstMentioned: turnId,
134
+ lastMentioned: turnId,
135
+ frequency: 0,
136
+ coOccurrences: [],
137
+ };
138
+ conv.knowledgeTree.push(node);
139
+ }
140
+ node.lastMentioned = turnId;
141
+ node.frequency++;
142
+ // Determine role from fields
143
+ if (fields) {
144
+ for (const [role, value] of Object.entries(fields)) {
145
+ if (String(value) === key) {
146
+ node.lastRole = role;
147
+ }
148
+ }
149
+ }
150
+ // Track co-occurrences with other entities in same turn
151
+ const turnEntities = conv.turns
152
+ .find((t) => t.id === turnId)
153
+ ?.entities.map((e) => e.resolved ?? e.text)
154
+ .filter((e) => e !== key) ?? [];
155
+ for (const co of turnEntities) {
156
+ if (!node.coOccurrences.includes(co)) {
157
+ node.coOccurrences.push(co);
158
+ }
159
+ }
160
+ }
161
+ /**
162
+ * Get the most recently mentioned entity of a given type.
163
+ */
164
+ export function getLastEntity(conv, type) {
165
+ return conv.knowledgeTree
166
+ .filter((n) => n.type === type)
167
+ .sort((a, b) => b.lastMentioned - a.lastMentioned)[0];
168
+ }
169
+ /**
170
+ * Get all entities, sorted by recency.
171
+ */
172
+ export function getRecentEntities(conv, limit = 10) {
173
+ return [...conv.knowledgeTree]
174
+ .sort((a, b) => b.lastMentioned - a.lastMentioned)
175
+ .slice(0, limit);
176
+ }
177
+ /**
178
+ * Get the last N user turns.
179
+ */
180
+ export function getRecentTurns(conv, count = 5) {
181
+ return conv.turns
182
+ .filter((t) => t.role === "user")
183
+ .slice(-count);
184
+ }
@@ -0,0 +1,11 @@
1
+ export declare function gitStatus(path?: string): Promise<string>;
2
+ export declare function gitLog(path?: string, count?: number): Promise<string>;
3
+ export declare function gitDiff(path?: string, target?: string): Promise<string>;
4
+ export declare function gitPull(path?: string, remote?: string, branch?: string): Promise<string>;
5
+ export declare function gitPush(path?: string, remote?: string, branch?: string): Promise<string>;
6
+ export declare function gitBranch(path?: string): Promise<string>;
7
+ export declare function gitCheckout(branch: string, path?: string): Promise<string>;
8
+ export declare function gitCommit(message: string, path?: string): Promise<string>;
9
+ export declare function gitAdd(target?: string, path?: string): Promise<string>;
10
+ export declare function gitStash(action?: string, path?: string): Promise<string>;
11
+ export declare function gitReset(target?: string, path?: string): Promise<string>;
@@ -0,0 +1,146 @@
1
+ import { simpleGit } from "simple-git";
2
+ /**
3
+ * Git execution layer using simple-git.
4
+ *
5
+ * Provides programmatic git operations with richer output
6
+ * than raw shell commands. Used by the executor when an
7
+ * intent is a git.* intent.
8
+ */
9
+ function getGit(path = ".") {
10
+ return simpleGit(path);
11
+ }
12
+ export async function gitStatus(path = ".") {
13
+ const git = getGit(path);
14
+ const status = await git.status();
15
+ const lines = [];
16
+ lines.push(`Branch: ${status.current ?? "detached"}`);
17
+ if (status.tracking) {
18
+ lines.push(`Tracking: ${status.tracking}`);
19
+ if (status.ahead > 0)
20
+ lines.push(` Ahead: ${status.ahead} commit(s)`);
21
+ if (status.behind > 0)
22
+ lines.push(` Behind: ${status.behind} commit(s)`);
23
+ }
24
+ if (status.staged.length > 0) {
25
+ lines.push(`\nStaged (${status.staged.length}):`);
26
+ for (const f of status.staged)
27
+ lines.push(` + ${f}`);
28
+ }
29
+ if (status.modified.length > 0) {
30
+ lines.push(`\nModified (${status.modified.length}):`);
31
+ for (const f of status.modified)
32
+ lines.push(` ~ ${f}`);
33
+ }
34
+ if (status.not_added.length > 0) {
35
+ lines.push(`\nUntracked (${status.not_added.length}):`);
36
+ for (const f of status.not_added)
37
+ lines.push(` ? ${f}`);
38
+ }
39
+ if (status.deleted.length > 0) {
40
+ lines.push(`\nDeleted (${status.deleted.length}):`);
41
+ for (const f of status.deleted)
42
+ lines.push(` - ${f}`);
43
+ }
44
+ if (status.conflicted.length > 0) {
45
+ lines.push(`\nConflicted (${status.conflicted.length}):`);
46
+ for (const f of status.conflicted)
47
+ lines.push(` ! ${f}`);
48
+ }
49
+ if (status.staged.length === 0 &&
50
+ status.modified.length === 0 &&
51
+ status.not_added.length === 0 &&
52
+ status.deleted.length === 0) {
53
+ lines.push("\nWorking tree clean.");
54
+ }
55
+ return lines.join("\n");
56
+ }
57
+ export async function gitLog(path = ".", count = 10) {
58
+ const git = getGit(path);
59
+ const log = await git.log({ maxCount: count });
60
+ const lines = [];
61
+ for (const entry of log.all) {
62
+ const date = entry.date.split("T")[0] ?? entry.date;
63
+ const hash = entry.hash.slice(0, 7);
64
+ lines.push(`${hash} ${date} ${entry.message} (${entry.author_name})`);
65
+ }
66
+ return lines.join("\n") || "No commits found.";
67
+ }
68
+ export async function gitDiff(path = ".", target) {
69
+ const git = getGit(path);
70
+ const args = target ? [target] : [];
71
+ const diff = await git.diff(args);
72
+ return diff || "No differences.";
73
+ }
74
+ export async function gitPull(path = ".", remote = "origin", branch) {
75
+ const git = getGit(path);
76
+ const result = await git.pull(remote, branch);
77
+ const lines = [];
78
+ if (result.summary.changes)
79
+ lines.push(`Changes: ${result.summary.changes}`);
80
+ if (result.summary.insertions)
81
+ lines.push(`Insertions: ${result.summary.insertions}`);
82
+ if (result.summary.deletions)
83
+ lines.push(`Deletions: ${result.summary.deletions}`);
84
+ if (result.files.length > 0) {
85
+ lines.push(`\nFiles updated (${result.files.length}):`);
86
+ for (const f of result.files)
87
+ lines.push(` ${f}`);
88
+ }
89
+ return lines.join("\n") || "Already up to date.";
90
+ }
91
+ export async function gitPush(path = ".", remote = "origin", branch) {
92
+ const git = getGit(path);
93
+ const result = await git.push(remote, branch);
94
+ const lines = [];
95
+ if (result.pushed.length > 0) {
96
+ for (const p of result.pushed) {
97
+ lines.push(`Pushed: ${p.local} → ${p.remote}`);
98
+ }
99
+ }
100
+ return lines.join("\n") || "Push complete.";
101
+ }
102
+ export async function gitBranch(path = ".") {
103
+ const git = getGit(path);
104
+ const branches = await git.branch();
105
+ const lines = [];
106
+ lines.push(`Current: ${branches.current}`);
107
+ lines.push(`\nAll branches:`);
108
+ for (const name of branches.all) {
109
+ const prefix = name === branches.current ? "* " : " ";
110
+ lines.push(`${prefix}${name}`);
111
+ }
112
+ return lines.join("\n");
113
+ }
114
+ export async function gitCheckout(branch, path = ".") {
115
+ const git = getGit(path);
116
+ await git.checkout(branch);
117
+ return `Switched to branch: ${branch}`;
118
+ }
119
+ export async function gitCommit(message, path = ".") {
120
+ const git = getGit(path);
121
+ const result = await git.commit(message);
122
+ return `Committed: ${result.commit} — ${message}\n${result.summary.changes} file(s) changed, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions`;
123
+ }
124
+ export async function gitAdd(target = ".", path = ".") {
125
+ const git = getGit(path);
126
+ await git.add(target);
127
+ return `Staged: ${target}`;
128
+ }
129
+ export async function gitStash(action = "push", path = ".") {
130
+ const git = getGit(path);
131
+ if (action === "pop" || action === "restore") {
132
+ const result = await git.stash(["pop"]);
133
+ return result || "Stash popped.";
134
+ }
135
+ if (action === "list") {
136
+ const result = await git.stash(["list"]);
137
+ return result || "No stashes.";
138
+ }
139
+ const result = await git.stash(["push"]);
140
+ return result || "Changes stashed.";
141
+ }
142
+ export async function gitReset(target = "HEAD", path = ".") {
143
+ const git = getGit(path);
144
+ await git.reset([target]);
145
+ return `Reset to: ${target}`;
146
+ }
@@ -0,0 +1,2 @@
1
+ export declare function runRemoteCommand(environment: string, command: string): Promise<string>;
2
+ export declare function runLocalCommand(command: string): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { loadHosts } from "../utils/config.js";
4
+ const execAsync = promisify(exec);
5
+ export async function runRemoteCommand(environment, command) {
6
+ const hosts = loadHosts();
7
+ const entry = hosts[environment];
8
+ if (!entry) {
9
+ throw new Error(`No host configured for environment: ${environment}`);
10
+ }
11
+ const { stdout, stderr } = await execAsync(`ssh ${entry.host} ${JSON.stringify(command)}`, { timeout: 30_000 });
12
+ return stderr ? `${stdout}\n${stderr}` : stdout;
13
+ }
14
+ export async function runLocalCommand(command) {
15
+ const { stdout, stderr } = await execAsync(command, { timeout: 30_000 });
16
+ return stderr ? `${stdout}\n${stderr}` : stdout;
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { DynamicIntent } from "../types/intent.js";
2
+ /**
3
+ * Generic command executor.
4
+ *
5
+ * For git.* intents, uses simple-git for richer programmatic output.
6
+ * For everything else, interpolates command templates and runs via shell.
7
+ */
8
+ export declare function executeIntent(intent: DynamicIntent): Promise<string>;