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.
- package/config/file-hints.json +255 -0
- package/config/hosts.json +14 -0
- package/config/intents.json +3920 -0
- package/config/playbooks.json +112 -0
- package/config/rules.json +100 -0
- package/dist/agents/agentSpawner.d.ts +56 -0
- package/dist/agents/agentSpawner.js +180 -0
- package/dist/agents/planner.d.ts +40 -0
- package/dist/agents/planner.js +175 -0
- package/dist/agents/playbookRunner.d.ts +45 -0
- package/dist/agents/playbookRunner.js +120 -0
- package/dist/agents/taskRunner.d.ts +61 -0
- package/dist/agents/taskRunner.js +142 -0
- package/dist/context/history.d.ts +36 -0
- package/dist/context/history.js +115 -0
- package/dist/conversation/coreference.d.ts +27 -0
- package/dist/conversation/coreference.js +147 -0
- package/dist/conversation/secrets.d.ts +43 -0
- package/dist/conversation/secrets.js +129 -0
- package/dist/conversation/store.d.ts +94 -0
- package/dist/conversation/store.js +184 -0
- package/dist/execution/git.d.ts +11 -0
- package/dist/execution/git.js +146 -0
- package/dist/execution/ssh.d.ts +2 -0
- package/dist/execution/ssh.js +17 -0
- package/dist/handlers/executor.d.ts +8 -0
- package/dist/handlers/executor.js +216 -0
- package/dist/healing/claudeHealer.d.ts +17 -0
- package/dist/healing/claudeHealer.js +300 -0
- package/dist/healing/patchPromoter.d.ts +25 -0
- package/dist/healing/patchPromoter.js +118 -0
- package/dist/healing/ruleBuilder.d.ts +5 -0
- package/dist/healing/ruleBuilder.js +111 -0
- package/dist/healing/ruleRepairer.d.ts +8 -0
- package/dist/healing/ruleRepairer.js +29 -0
- package/dist/healing/ruleValidator.d.ts +22 -0
- package/dist/healing/ruleValidator.js +145 -0
- package/dist/healing/runHealer.d.ts +11 -0
- package/dist/healing/runHealer.js +74 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +62 -0
- package/dist/intents/catalog.d.ts +4 -0
- package/dist/intents/catalog.js +7 -0
- package/dist/nlp/disambiguate.d.ts +2 -0
- package/dist/nlp/disambiguate.js +46 -0
- package/dist/nlp/fuzzyResolver.d.ts +14 -0
- package/dist/nlp/fuzzyResolver.js +108 -0
- package/dist/nlp/llmFallback.d.ts +63 -0
- package/dist/nlp/llmFallback.js +338 -0
- package/dist/nlp/llmParser.d.ts +8 -0
- package/dist/nlp/llmParser.js +118 -0
- package/dist/nlp/multiClassifier.d.ts +39 -0
- package/dist/nlp/multiClassifier.js +181 -0
- package/dist/nlp/parseIntent.d.ts +2 -0
- package/dist/nlp/parseIntent.js +34 -0
- package/dist/nlp/ruleParser.d.ts +2 -0
- package/dist/nlp/ruleParser.js +234 -0
- package/dist/nlp/semantic.d.ts +104 -0
- package/dist/nlp/semantic.js +419 -0
- package/dist/nlp/uncertainty.d.ts +42 -0
- package/dist/nlp/uncertainty.js +103 -0
- package/dist/parsers/apacheParser.d.ts +50 -0
- package/dist/parsers/apacheParser.js +152 -0
- package/dist/parsers/bindParser.d.ts +40 -0
- package/dist/parsers/bindParser.js +189 -0
- package/dist/parsers/envFile.d.ts +39 -0
- package/dist/parsers/envFile.js +128 -0
- package/dist/parsers/fileFinder.d.ts +30 -0
- package/dist/parsers/fileFinder.js +226 -0
- package/dist/parsers/index.d.ts +27 -0
- package/dist/parsers/index.js +193 -0
- package/dist/parsers/jsonParser.d.ts +16 -0
- package/dist/parsers/jsonParser.js +57 -0
- package/dist/parsers/nginxParser.d.ts +47 -0
- package/dist/parsers/nginxParser.js +161 -0
- package/dist/parsers/passwd.d.ts +25 -0
- package/dist/parsers/passwd.js +41 -0
- package/dist/parsers/shadow.d.ts +23 -0
- package/dist/parsers/shadow.js +50 -0
- package/dist/parsers/yamlParser.d.ts +13 -0
- package/dist/parsers/yamlParser.js +54 -0
- package/dist/policy/confirm.d.ts +2 -0
- package/dist/policy/confirm.js +29 -0
- package/dist/policy/safety.d.ts +4 -0
- package/dist/policy/safety.js +32 -0
- package/dist/types/intent.d.ts +205 -0
- package/dist/types/intent.js +32 -0
- package/dist/types/rules.d.ts +237 -0
- package/dist/types/rules.js +50 -0
- package/dist/utils/analysis.d.ts +25 -0
- package/dist/utils/analysis.js +307 -0
- package/dist/utils/autoBackup.d.ts +43 -0
- package/dist/utils/autoBackup.js +144 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +32 -0
- package/dist/utils/dirAnalysis.d.ts +23 -0
- package/dist/utils/dirAnalysis.js +192 -0
- package/dist/utils/explain.d.ts +8 -0
- package/dist/utils/explain.js +145 -0
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +26 -0
- package/dist/utils/paths.d.ts +26 -0
- package/dist/utils/paths.js +47 -0
- package/dist/utils/permissions.d.ts +64 -0
- package/dist/utils/permissions.js +298 -0
- package/dist/utils/platform.d.ts +53 -0
- package/dist/utils/platform.js +253 -0
- package/dist/utils/smartFile.d.ts +29 -0
- package/dist/utils/smartFile.js +188 -0
- package/dist/utils/spinner.d.ts +53 -0
- package/dist/utils/spinner.js +140 -0
- package/dist/utils/verbose.d.ts +27 -0
- package/dist/utils/verbose.js +131 -0
- package/dist/utils/wslPaths.d.ts +31 -0
- package/dist/utils/wslPaths.js +145 -0
- package/package.json +39 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook runner.
|
|
3
|
+
*
|
|
4
|
+
* Loads playbooks from config/playbooks.json and executes them
|
|
5
|
+
* step by step, locally or remotely.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlaybookStep {
|
|
8
|
+
command: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Playbook {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
steps: PlaybookStep[];
|
|
15
|
+
}
|
|
16
|
+
export interface PlaybookResult {
|
|
17
|
+
name: string;
|
|
18
|
+
environment: string;
|
|
19
|
+
steps: Array<{
|
|
20
|
+
label: string;
|
|
21
|
+
command: string;
|
|
22
|
+
output: string;
|
|
23
|
+
success: boolean;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}>;
|
|
26
|
+
totalDurationMs: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load all playbooks from config.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadPlaybooks(): Playbook[];
|
|
32
|
+
/**
|
|
33
|
+
* Get a playbook by name.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPlaybook(name: string): Playbook | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* List all available playbooks.
|
|
38
|
+
*/
|
|
39
|
+
export declare function formatPlaybookList(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Execute a playbook step by step.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runPlaybook(playbook: Playbook, environment?: string, options?: {
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
}): Promise<PlaybookResult>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbook runner.
|
|
3
|
+
*
|
|
4
|
+
* Loads playbooks from config/playbooks.json and executes them
|
|
5
|
+
* step by step, locally or remotely.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { runRemoteCommand, runLocalCommand } from "../execution/ssh.js";
|
|
10
|
+
import { CONFIG_DIR } from "../utils/paths.js";
|
|
11
|
+
const PLAYBOOKS_FILE = resolve(CONFIG_DIR, "playbooks.json");
|
|
12
|
+
const c = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
red: "\x1b[31m",
|
|
19
|
+
cyan: "\x1b[36m",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Load all playbooks from config.
|
|
23
|
+
*/
|
|
24
|
+
export function loadPlaybooks() {
|
|
25
|
+
if (!existsSync(PLAYBOOKS_FILE))
|
|
26
|
+
return [];
|
|
27
|
+
const raw = JSON.parse(readFileSync(PLAYBOOKS_FILE, "utf-8"));
|
|
28
|
+
return raw.playbooks ?? [];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get a playbook by name.
|
|
32
|
+
*/
|
|
33
|
+
export function getPlaybook(name) {
|
|
34
|
+
return loadPlaybooks().find((p) => p.name === name || p.name.includes(name));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* List all available playbooks.
|
|
38
|
+
*/
|
|
39
|
+
export function formatPlaybookList() {
|
|
40
|
+
const playbooks = loadPlaybooks();
|
|
41
|
+
if (playbooks.length === 0)
|
|
42
|
+
return `${c.dim}No playbooks configured.${c.reset}`;
|
|
43
|
+
const lines = [`${c.bold}Available playbooks:${c.reset}\n`];
|
|
44
|
+
for (const pb of playbooks) {
|
|
45
|
+
lines.push(` ${c.cyan}${pb.name}${c.reset} — ${pb.description} (${pb.steps.length} steps)`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(`\n ${c.dim}Run: :play <name> [environment]${c.reset}`);
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Execute a playbook step by step.
|
|
52
|
+
*/
|
|
53
|
+
export async function runPlaybook(playbook, environment, options = {}) {
|
|
54
|
+
const env = environment ?? "dev";
|
|
55
|
+
const isRemote = env !== "local";
|
|
56
|
+
console.log(`\n${c.bold}${c.cyan}Playbook: ${playbook.name}${c.reset}`);
|
|
57
|
+
console.log(`${c.dim}${playbook.description}${c.reset}`);
|
|
58
|
+
console.log(`${c.dim}Target: ${env} | ${playbook.steps.length} steps${c.reset}\n`);
|
|
59
|
+
const result = {
|
|
60
|
+
name: playbook.name,
|
|
61
|
+
environment: env,
|
|
62
|
+
steps: [],
|
|
63
|
+
totalDurationMs: 0,
|
|
64
|
+
};
|
|
65
|
+
const totalStart = Date.now();
|
|
66
|
+
for (let i = 0; i < playbook.steps.length; i++) {
|
|
67
|
+
const step = playbook.steps[i];
|
|
68
|
+
const stepNum = `[${i + 1}/${playbook.steps.length}]`;
|
|
69
|
+
console.log(`${c.cyan}${stepNum}${c.reset} ${step.label}...`);
|
|
70
|
+
if (options.dryRun) {
|
|
71
|
+
console.log(` ${c.dim}$ ${step.command}${c.reset}\n`);
|
|
72
|
+
result.steps.push({
|
|
73
|
+
label: step.label,
|
|
74
|
+
command: step.command,
|
|
75
|
+
output: "[dry-run]",
|
|
76
|
+
success: true,
|
|
77
|
+
durationMs: 0,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
try {
|
|
83
|
+
const output = isRemote
|
|
84
|
+
? await runRemoteCommand(env, step.command)
|
|
85
|
+
: await runLocalCommand(step.command);
|
|
86
|
+
const duration = Date.now() - start;
|
|
87
|
+
// Indent output
|
|
88
|
+
const indented = output.trim().split("\n").map((l) => ` ${l}`).join("\n");
|
|
89
|
+
console.log(`${indented}`);
|
|
90
|
+
console.log(` ${c.green}✓${c.reset} ${c.dim}(${duration}ms)${c.reset}\n`);
|
|
91
|
+
result.steps.push({
|
|
92
|
+
label: step.label,
|
|
93
|
+
command: step.command,
|
|
94
|
+
output: output.trim(),
|
|
95
|
+
success: true,
|
|
96
|
+
durationMs: duration,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const duration = Date.now() - start;
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
console.log(` ${c.red}✗ ${msg.split("\n")[0]}${c.reset}`);
|
|
103
|
+
console.log(` ${c.dim}(${duration}ms)${c.reset}\n`);
|
|
104
|
+
result.steps.push({
|
|
105
|
+
label: step.label,
|
|
106
|
+
command: step.command,
|
|
107
|
+
output: msg,
|
|
108
|
+
success: false,
|
|
109
|
+
durationMs: duration,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
result.totalDurationMs = Date.now() - totalStart;
|
|
114
|
+
// Summary
|
|
115
|
+
const passed = result.steps.filter((s) => s.success).length;
|
|
116
|
+
const failed = result.steps.filter((s) => !s.success).length;
|
|
117
|
+
const icon = failed === 0 ? `${c.green}✓${c.reset}` : `${c.yellow}⚠${c.reset}`;
|
|
118
|
+
console.log(`${icon} ${c.bold}Playbook complete:${c.reset} ${passed} passed, ${failed} failed (${(result.totalDurationMs / 1000).toFixed(1)}s)`);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { DynamicIntent } from "../types/intent.js";
|
|
3
|
+
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
4
|
+
export interface BackgroundTask {
|
|
5
|
+
id: number;
|
|
6
|
+
rawText: string;
|
|
7
|
+
intent: DynamicIntent;
|
|
8
|
+
status: TaskStatus;
|
|
9
|
+
startedAt: Date;
|
|
10
|
+
completedAt?: Date;
|
|
11
|
+
result?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
/** If true, the user has seen the completion notification */
|
|
14
|
+
acknowledged: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* TaskRunner — manages background execution of CLI commands.
|
|
18
|
+
*
|
|
19
|
+
* Emits events:
|
|
20
|
+
* "task:started" (task)
|
|
21
|
+
* "task:completed" (task)
|
|
22
|
+
* "task:failed" (task)
|
|
23
|
+
*
|
|
24
|
+
* The interactive REPL listens to these events and prints
|
|
25
|
+
* notifications between prompts.
|
|
26
|
+
*/
|
|
27
|
+
export declare class TaskRunner extends EventEmitter {
|
|
28
|
+
private tasks;
|
|
29
|
+
private nextId;
|
|
30
|
+
private maxConcurrent;
|
|
31
|
+
private runningCount;
|
|
32
|
+
private queue;
|
|
33
|
+
constructor(maxConcurrent?: number);
|
|
34
|
+
/**
|
|
35
|
+
* Submit a task for background execution.
|
|
36
|
+
* Returns the task ID immediately.
|
|
37
|
+
*/
|
|
38
|
+
submit(rawText: string, intent: DynamicIntent, executor: () => Promise<string>): BackgroundTask;
|
|
39
|
+
private run;
|
|
40
|
+
private drainQueue;
|
|
41
|
+
/** Cancel a pending or running task (best-effort). */
|
|
42
|
+
cancel(id: number): boolean;
|
|
43
|
+
/** Get a task by ID. */
|
|
44
|
+
get(id: number): BackgroundTask | undefined;
|
|
45
|
+
/** List all tasks, optionally filtered by status. */
|
|
46
|
+
list(filter?: TaskStatus): BackgroundTask[];
|
|
47
|
+
/** Get tasks that completed since the user last checked. */
|
|
48
|
+
getUnacknowledged(): BackgroundTask[];
|
|
49
|
+
/** Mark a task as seen by the user. */
|
|
50
|
+
acknowledge(id: number): void;
|
|
51
|
+
/** Acknowledge all completed tasks. */
|
|
52
|
+
acknowledgeAll(): void;
|
|
53
|
+
/** Clear completed/failed/cancelled tasks from the list. */
|
|
54
|
+
prune(): number;
|
|
55
|
+
/** How many tasks are currently running? */
|
|
56
|
+
get active(): number;
|
|
57
|
+
/** How many tasks are in the queue? */
|
|
58
|
+
get queued(): number;
|
|
59
|
+
}
|
|
60
|
+
/** Singleton instance used by the interactive CLI. */
|
|
61
|
+
export declare const taskRunner: TaskRunner;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* TaskRunner — manages background execution of CLI commands.
|
|
4
|
+
*
|
|
5
|
+
* Emits events:
|
|
6
|
+
* "task:started" (task)
|
|
7
|
+
* "task:completed" (task)
|
|
8
|
+
* "task:failed" (task)
|
|
9
|
+
*
|
|
10
|
+
* The interactive REPL listens to these events and prints
|
|
11
|
+
* notifications between prompts.
|
|
12
|
+
*/
|
|
13
|
+
export class TaskRunner extends EventEmitter {
|
|
14
|
+
tasks = new Map();
|
|
15
|
+
nextId = 1;
|
|
16
|
+
maxConcurrent;
|
|
17
|
+
runningCount = 0;
|
|
18
|
+
queue = [];
|
|
19
|
+
constructor(maxConcurrent = 5) {
|
|
20
|
+
super();
|
|
21
|
+
this.maxConcurrent = maxConcurrent;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Submit a task for background execution.
|
|
25
|
+
* Returns the task ID immediately.
|
|
26
|
+
*/
|
|
27
|
+
submit(rawText, intent, executor) {
|
|
28
|
+
const task = {
|
|
29
|
+
id: this.nextId++,
|
|
30
|
+
rawText,
|
|
31
|
+
intent,
|
|
32
|
+
status: "pending",
|
|
33
|
+
startedAt: new Date(),
|
|
34
|
+
acknowledged: false,
|
|
35
|
+
};
|
|
36
|
+
this.tasks.set(task.id, task);
|
|
37
|
+
if (this.runningCount < this.maxConcurrent) {
|
|
38
|
+
this.run(task, executor);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.queue.push({ task, executor });
|
|
42
|
+
this.emit("task:queued", task);
|
|
43
|
+
}
|
|
44
|
+
return task;
|
|
45
|
+
}
|
|
46
|
+
async run(task, executor) {
|
|
47
|
+
task.status = "running";
|
|
48
|
+
this.runningCount++;
|
|
49
|
+
this.emit("task:started", task);
|
|
50
|
+
try {
|
|
51
|
+
const result = await executor();
|
|
52
|
+
task.status = "completed";
|
|
53
|
+
task.result = result;
|
|
54
|
+
task.completedAt = new Date();
|
|
55
|
+
this.emit("task:completed", task);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
task.status = "failed";
|
|
59
|
+
task.error = err instanceof Error ? err.message : String(err);
|
|
60
|
+
task.completedAt = new Date();
|
|
61
|
+
this.emit("task:failed", task);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.runningCount--;
|
|
65
|
+
this.drainQueue();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
drainQueue() {
|
|
69
|
+
while (this.queue.length > 0 && this.runningCount < this.maxConcurrent) {
|
|
70
|
+
const next = this.queue.shift();
|
|
71
|
+
this.run(next.task, next.executor);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Cancel a pending or running task (best-effort). */
|
|
75
|
+
cancel(id) {
|
|
76
|
+
const task = this.tasks.get(id);
|
|
77
|
+
if (!task)
|
|
78
|
+
return false;
|
|
79
|
+
if (task.status === "pending") {
|
|
80
|
+
task.status = "cancelled";
|
|
81
|
+
task.completedAt = new Date();
|
|
82
|
+
this.queue = this.queue.filter((q) => q.task.id !== id);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// Running tasks can't truly be cancelled without AbortController,
|
|
86
|
+
// but we mark them so the user knows.
|
|
87
|
+
if (task.status === "running") {
|
|
88
|
+
task.status = "cancelled";
|
|
89
|
+
task.completedAt = new Date();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/** Get a task by ID. */
|
|
95
|
+
get(id) {
|
|
96
|
+
return this.tasks.get(id);
|
|
97
|
+
}
|
|
98
|
+
/** List all tasks, optionally filtered by status. */
|
|
99
|
+
list(filter) {
|
|
100
|
+
const all = Array.from(this.tasks.values());
|
|
101
|
+
return filter ? all.filter((t) => t.status === filter) : all;
|
|
102
|
+
}
|
|
103
|
+
/** Get tasks that completed since the user last checked. */
|
|
104
|
+
getUnacknowledged() {
|
|
105
|
+
return Array.from(this.tasks.values()).filter((t) => !t.acknowledged && (t.status === "completed" || t.status === "failed"));
|
|
106
|
+
}
|
|
107
|
+
/** Mark a task as seen by the user. */
|
|
108
|
+
acknowledge(id) {
|
|
109
|
+
const task = this.tasks.get(id);
|
|
110
|
+
if (task)
|
|
111
|
+
task.acknowledged = true;
|
|
112
|
+
}
|
|
113
|
+
/** Acknowledge all completed tasks. */
|
|
114
|
+
acknowledgeAll() {
|
|
115
|
+
for (const task of this.tasks.values()) {
|
|
116
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
117
|
+
task.acknowledged = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Clear completed/failed/cancelled tasks from the list. */
|
|
122
|
+
prune() {
|
|
123
|
+
let pruned = 0;
|
|
124
|
+
for (const [id, task] of this.tasks.entries()) {
|
|
125
|
+
if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
|
|
126
|
+
this.tasks.delete(id);
|
|
127
|
+
pruned++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return pruned;
|
|
131
|
+
}
|
|
132
|
+
/** How many tasks are currently running? */
|
|
133
|
+
get active() {
|
|
134
|
+
return this.runningCount;
|
|
135
|
+
}
|
|
136
|
+
/** How many tasks are in the queue? */
|
|
137
|
+
get queued() {
|
|
138
|
+
return this.queue.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Singleton instance used by the interactive CLI. */
|
|
142
|
+
export const taskRunner = new TaskRunner();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface HistoryEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
rawText: string;
|
|
4
|
+
intent: string;
|
|
5
|
+
fields: Record<string, unknown>;
|
|
6
|
+
command: string;
|
|
7
|
+
environment: string;
|
|
8
|
+
success: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SessionContext {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
lastActivity: string;
|
|
14
|
+
recentIntents: string[];
|
|
15
|
+
recentEnvironments: string[];
|
|
16
|
+
recentServices: string[];
|
|
17
|
+
variables: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export declare function recordHistory(entry: HistoryEntry): void;
|
|
20
|
+
export declare function loadHistory(): HistoryEntry[];
|
|
21
|
+
export declare function getRecentHistory(count?: number): HistoryEntry[];
|
|
22
|
+
export declare function searchHistory(query: string): HistoryEntry[];
|
|
23
|
+
export declare function clearHistory(): void;
|
|
24
|
+
export declare function getSession(): SessionContext;
|
|
25
|
+
export declare function setVariable(key: string, value: unknown): void;
|
|
26
|
+
export declare function getVariable(key: string): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Get context hints for the parser.
|
|
29
|
+
*
|
|
30
|
+
* Returns the most likely environment and service based on recent activity,
|
|
31
|
+
* so the parser can use these as smart defaults.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getContextHints(): {
|
|
34
|
+
environment?: string;
|
|
35
|
+
service?: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { DATA_DIR } from "../utils/paths.js";
|
|
4
|
+
const HISTORY_FILE = resolve(DATA_DIR, "history.json");
|
|
5
|
+
const SESSION_FILE = resolve(DATA_DIR, "session.json");
|
|
6
|
+
const MAX_HISTORY = 500;
|
|
7
|
+
function ensureDataDir() {
|
|
8
|
+
if (!existsSync(DATA_DIR)) {
|
|
9
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// ─── History ─────────────────────────────────────────────────────────────────
|
|
13
|
+
export function recordHistory(entry) {
|
|
14
|
+
ensureDataDir();
|
|
15
|
+
const history = loadHistory();
|
|
16
|
+
history.push(entry);
|
|
17
|
+
// Trim to max
|
|
18
|
+
if (history.length > MAX_HISTORY) {
|
|
19
|
+
history.splice(0, history.length - MAX_HISTORY);
|
|
20
|
+
}
|
|
21
|
+
writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
|
|
22
|
+
// Also update session
|
|
23
|
+
updateSession(entry);
|
|
24
|
+
}
|
|
25
|
+
export function loadHistory() {
|
|
26
|
+
if (!existsSync(HISTORY_FILE))
|
|
27
|
+
return [];
|
|
28
|
+
const raw = readFileSync(HISTORY_FILE, "utf-8");
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
export function getRecentHistory(count = 10) {
|
|
32
|
+
const history = loadHistory();
|
|
33
|
+
return history.slice(-count);
|
|
34
|
+
}
|
|
35
|
+
export function searchHistory(query) {
|
|
36
|
+
const history = loadHistory();
|
|
37
|
+
const lower = query.toLowerCase();
|
|
38
|
+
return history.filter((h) => h.rawText.toLowerCase().includes(lower) ||
|
|
39
|
+
h.intent.includes(lower) ||
|
|
40
|
+
h.command.toLowerCase().includes(lower));
|
|
41
|
+
}
|
|
42
|
+
export function clearHistory() {
|
|
43
|
+
ensureDataDir();
|
|
44
|
+
writeFileSync(HISTORY_FILE, "[]");
|
|
45
|
+
}
|
|
46
|
+
// ─── Session Context ─────────────────────────────────────────────────────────
|
|
47
|
+
function generateSessionId() {
|
|
48
|
+
return `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
49
|
+
}
|
|
50
|
+
export function getSession() {
|
|
51
|
+
ensureDataDir();
|
|
52
|
+
if (existsSync(SESSION_FILE)) {
|
|
53
|
+
const raw = readFileSync(SESSION_FILE, "utf-8");
|
|
54
|
+
const session = JSON.parse(raw);
|
|
55
|
+
// If session is older than 1 hour, start new
|
|
56
|
+
const age = Date.now() - new Date(session.lastActivity).getTime();
|
|
57
|
+
if (age < 3600_000)
|
|
58
|
+
return session;
|
|
59
|
+
}
|
|
60
|
+
const session = {
|
|
61
|
+
sessionId: generateSessionId(),
|
|
62
|
+
startedAt: new Date().toISOString(),
|
|
63
|
+
lastActivity: new Date().toISOString(),
|
|
64
|
+
recentIntents: [],
|
|
65
|
+
recentEnvironments: [],
|
|
66
|
+
recentServices: [],
|
|
67
|
+
variables: {},
|
|
68
|
+
};
|
|
69
|
+
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
function updateSession(entry) {
|
|
73
|
+
const session = getSession();
|
|
74
|
+
session.lastActivity = new Date().toISOString();
|
|
75
|
+
// Track recent intents (last 10)
|
|
76
|
+
session.recentIntents.push(entry.intent);
|
|
77
|
+
if (session.recentIntents.length > 10)
|
|
78
|
+
session.recentIntents.shift();
|
|
79
|
+
// Track recent environments
|
|
80
|
+
if (entry.environment && !session.recentEnvironments.includes(entry.environment)) {
|
|
81
|
+
session.recentEnvironments.push(entry.environment);
|
|
82
|
+
if (session.recentEnvironments.length > 5)
|
|
83
|
+
session.recentEnvironments.shift();
|
|
84
|
+
}
|
|
85
|
+
// Track recent services
|
|
86
|
+
const service = entry.fields.service;
|
|
87
|
+
if (service && !session.recentServices.includes(service)) {
|
|
88
|
+
session.recentServices.push(service);
|
|
89
|
+
if (session.recentServices.length > 5)
|
|
90
|
+
session.recentServices.shift();
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
93
|
+
}
|
|
94
|
+
export function setVariable(key, value) {
|
|
95
|
+
const session = getSession();
|
|
96
|
+
session.variables[key] = value;
|
|
97
|
+
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
98
|
+
}
|
|
99
|
+
export function getVariable(key) {
|
|
100
|
+
const session = getSession();
|
|
101
|
+
return session.variables[key];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get context hints for the parser.
|
|
105
|
+
*
|
|
106
|
+
* Returns the most likely environment and service based on recent activity,
|
|
107
|
+
* so the parser can use these as smart defaults.
|
|
108
|
+
*/
|
|
109
|
+
export function getContextHints() {
|
|
110
|
+
const session = getSession();
|
|
111
|
+
return {
|
|
112
|
+
environment: session.recentEnvironments[session.recentEnvironments.length - 1],
|
|
113
|
+
service: session.recentServices[session.recentServices.length - 1],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Conversation } from "./store.js";
|
|
2
|
+
import type { DynamicIntent } from "../types/intent.js";
|
|
3
|
+
export interface CoreferenceResult {
|
|
4
|
+
/** The resolved text after pronoun replacement */
|
|
5
|
+
resolvedText: string;
|
|
6
|
+
/** Whether this is a repeat/reference to a previous command */
|
|
7
|
+
isReference: boolean;
|
|
8
|
+
/** The intent to use (from previous turn if reference) */
|
|
9
|
+
resolvedIntent?: DynamicIntent;
|
|
10
|
+
/** What was resolved and how */
|
|
11
|
+
resolutions: Array<{
|
|
12
|
+
original: string;
|
|
13
|
+
resolved: string;
|
|
14
|
+
source: "last_turn" | "knowledge_tree" | "pronoun";
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve coreferences in user input using conversation history.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveCoreferences(rawText: string, conv: Conversation): CoreferenceResult;
|
|
21
|
+
/**
|
|
22
|
+
* Extract entities from parsed fields for conversation tracking.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractEntitiesFromFields(fields: Record<string, unknown>): Array<{
|
|
25
|
+
text: string;
|
|
26
|
+
type: "service" | "environment" | "path" | "user" | "branch" | "container" | "unknown";
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getLastEntity, getRecentTurns } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Coreference resolver.
|
|
4
|
+
*
|
|
5
|
+
* Resolves pronouns and references like:
|
|
6
|
+
* "do it again" → repeat last command
|
|
7
|
+
* "same but on prod" → last command with environment changed
|
|
8
|
+
* "that service" → most recent service entity
|
|
9
|
+
* "it" → most recent object/service
|
|
10
|
+
* "do that on staging" → last intent with env=staging
|
|
11
|
+
* "restart it" → restart + most recent service
|
|
12
|
+
* "check the same one" → most recent service/target
|
|
13
|
+
*/
|
|
14
|
+
// Patterns that signal a reference to a previous turn
|
|
15
|
+
const REPEAT_PATTERNS = [
|
|
16
|
+
/^(do it|do that|run it|run that|same thing|again|repeat|redo|re-?run)\b/i,
|
|
17
|
+
/^same\b/i,
|
|
18
|
+
];
|
|
19
|
+
const PRONOUN_PATTERNS = [
|
|
20
|
+
{ pattern: /\bit\b/i, refType: "any" },
|
|
21
|
+
{ pattern: /\bthat service\b/i, refType: "service" },
|
|
22
|
+
{ pattern: /\bthat server\b/i, refType: "service" },
|
|
23
|
+
{ pattern: /\bthe same (one|service|server|thing)\b/i, refType: "service" },
|
|
24
|
+
{ pattern: /\bthat (file|path|directory)\b/i, refType: "path" },
|
|
25
|
+
{ pattern: /\bthere\b/i, refType: "environment" },
|
|
26
|
+
{ pattern: /\bthat (env|environment|box|machine)\b/i, refType: "environment" },
|
|
27
|
+
];
|
|
28
|
+
const OVERRIDE_PATTERNS = [
|
|
29
|
+
{ pattern: /\bbut (?:on|in) (\w+)\b/i, field: "environment" },
|
|
30
|
+
{ pattern: /\binstead (?:on|in) (\w+)\b/i, field: "environment" },
|
|
31
|
+
{ pattern: /\bon (\w+) instead\b/i, field: "environment" },
|
|
32
|
+
{ pattern: /\bbut (?:for|with) (\w+)\b/i, field: "service" },
|
|
33
|
+
{ pattern: /\binstead (?:of )?(\w+)\b/i, field: "service" },
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Resolve coreferences in user input using conversation history.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveCoreferences(rawText, conv) {
|
|
39
|
+
const resolutions = [];
|
|
40
|
+
let resolvedText = rawText;
|
|
41
|
+
let isReference = false;
|
|
42
|
+
let resolvedIntent;
|
|
43
|
+
const recentTurns = getRecentTurns(conv, 5);
|
|
44
|
+
const lastUserTurn = recentTurns[recentTurns.length - 1];
|
|
45
|
+
// 1. Check for full repeat patterns ("do it again", "same thing")
|
|
46
|
+
for (const pattern of REPEAT_PATTERNS) {
|
|
47
|
+
if (pattern.test(rawText.trim())) {
|
|
48
|
+
isReference = true;
|
|
49
|
+
if (lastUserTurn?.intent && lastUserTurn.fields) {
|
|
50
|
+
// Check for override modifiers ("same but on prod")
|
|
51
|
+
const overrides = extractOverrides(rawText);
|
|
52
|
+
const fields = { ...lastUserTurn.fields, ...overrides };
|
|
53
|
+
resolvedIntent = {
|
|
54
|
+
intent: lastUserTurn.intent,
|
|
55
|
+
confidence: 0.85,
|
|
56
|
+
rawText,
|
|
57
|
+
fields,
|
|
58
|
+
};
|
|
59
|
+
resolvedText = lastUserTurn.rawText;
|
|
60
|
+
resolutions.push({
|
|
61
|
+
original: rawText,
|
|
62
|
+
resolved: lastUserTurn.rawText,
|
|
63
|
+
source: "last_turn",
|
|
64
|
+
});
|
|
65
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
66
|
+
resolutions.push({
|
|
67
|
+
original: `override: ${key}`,
|
|
68
|
+
resolved: String(value),
|
|
69
|
+
source: "last_turn",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { resolvedText, isReference, resolvedIntent, resolutions };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 2. Resolve pronouns ("restart it", "check that service")
|
|
77
|
+
for (const { pattern, refType } of PRONOUN_PATTERNS) {
|
|
78
|
+
const match = rawText.match(pattern);
|
|
79
|
+
if (!match)
|
|
80
|
+
continue;
|
|
81
|
+
let resolved;
|
|
82
|
+
if (refType === "any") {
|
|
83
|
+
// "it" → most recent service, then most recent entity
|
|
84
|
+
resolved = getLastEntity(conv, "service")
|
|
85
|
+
?? getLastEntity(conv, "path")
|
|
86
|
+
?? getLastEntity(conv, "container");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
resolved = getLastEntity(conv, refType);
|
|
90
|
+
}
|
|
91
|
+
if (resolved) {
|
|
92
|
+
resolvedText = resolvedText.replace(match[0], resolved.entity);
|
|
93
|
+
resolutions.push({
|
|
94
|
+
original: match[0],
|
|
95
|
+
resolved: resolved.entity,
|
|
96
|
+
source: "knowledge_tree",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 3. Check for override modifiers in non-repeat contexts
|
|
101
|
+
// "restart nginx but on staging" → already has "restart nginx", just override env
|
|
102
|
+
const overrides = extractOverrides(rawText);
|
|
103
|
+
if (Object.keys(overrides).length > 0) {
|
|
104
|
+
// Clean the override phrases from the text
|
|
105
|
+
for (const { pattern } of OVERRIDE_PATTERNS) {
|
|
106
|
+
resolvedText = resolvedText.replace(pattern, "").trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { resolvedText, isReference, resolvedIntent, resolutions };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Extract field overrides from phrases like "but on prod", "instead on staging".
|
|
113
|
+
*/
|
|
114
|
+
function extractOverrides(text) {
|
|
115
|
+
const overrides = {};
|
|
116
|
+
for (const { pattern, field } of OVERRIDE_PATTERNS) {
|
|
117
|
+
const match = text.match(pattern);
|
|
118
|
+
if (match?.[1]) {
|
|
119
|
+
overrides[field] = match[1];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return overrides;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Extract entities from parsed fields for conversation tracking.
|
|
126
|
+
*/
|
|
127
|
+
export function extractEntitiesFromFields(fields) {
|
|
128
|
+
const entities = [];
|
|
129
|
+
const typeMap = {
|
|
130
|
+
service: "service",
|
|
131
|
+
environment: "environment",
|
|
132
|
+
path: "path",
|
|
133
|
+
source: "path",
|
|
134
|
+
destination: "path",
|
|
135
|
+
target: "path",
|
|
136
|
+
username: "user",
|
|
137
|
+
branch: "branch",
|
|
138
|
+
container: "container",
|
|
139
|
+
};
|
|
140
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
141
|
+
if (value && typeof value === "string" && value.length > 0) {
|
|
142
|
+
const type = typeMap[key] ?? "unknown";
|
|
143
|
+
entities.push({ text: value, type });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return entities;
|
|
147
|
+
}
|