kritya 0.8.2-beta
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/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/agent/budget.js +25 -0
- package/dist/agent/compactor.js +78 -0
- package/dist/agent/contextWarning.js +6 -0
- package/dist/agent/killSwitch.js +111 -0
- package/dist/agent/loop.js +543 -0
- package/dist/agent/memory.js +145 -0
- package/dist/agent/plugins.js +408 -0
- package/dist/agent/skills.js +210 -0
- package/dist/agent/skillsCli.js +51 -0
- package/dist/agent/systemPrompt.js +122 -0
- package/dist/agent/tokens.js +55 -0
- package/dist/agent/toolExecutor.js +331 -0
- package/dist/agent/workflow.js +496 -0
- package/dist/agent/worktree.js +116 -0
- package/dist/atomicWrite.js +109 -0
- package/dist/audit/audit.js +230 -0
- package/dist/audit/cli.js +135 -0
- package/dist/commands/custom.js +69 -0
- package/dist/commands/mcpCommand.js +433 -0
- package/dist/commands/pluginsCommand.js +204 -0
- package/dist/commands/registry.js +739 -0
- package/dist/config/config.js +214 -0
- package/dist/config/debug.js +19 -0
- package/dist/config/models.js +82 -0
- package/dist/config/retention.js +24 -0
- package/dist/config/winAcl.js +38 -0
- package/dist/crash.js +102 -0
- package/dist/engine.js +113 -0
- package/dist/git/git.js +46 -0
- package/dist/headless.js +274 -0
- package/dist/hooks/hooks.js +122 -0
- package/dist/index.js +670 -0
- package/dist/lsp/client.js +413 -0
- package/dist/lsp/manager.js +0 -0
- package/dist/lsp/registry.js +62 -0
- package/dist/mcp/callback.js +141 -0
- package/dist/mcp/client.js +945 -0
- package/dist/mcp/login.js +117 -0
- package/dist/mcp/oauth.js +345 -0
- package/dist/mcp/servers.js +114 -0
- package/dist/mcp/spawnWin.js +86 -0
- package/dist/mcp/tokens.js +97 -0
- package/dist/mcp/transport.js +295 -0
- package/dist/net/urlSafety.js +158 -0
- package/dist/permissions/danger.js +96 -0
- package/dist/permissions/permissions.js +62 -0
- package/dist/permissions/rules.js +69 -0
- package/dist/plugins/discover.js +108 -0
- package/dist/plugins/mcp.js +80 -0
- package/dist/provider/client.js +460 -0
- package/dist/provider/switchyardClient.js +46 -0
- package/dist/provider/switchyardSidecar.js +245 -0
- package/dist/provider/textToolCalls.js +140 -0
- package/dist/repomap/repoMap.js +168 -0
- package/dist/repomap/symbols.js +190 -0
- package/dist/session/store.js +328 -0
- package/dist/shell/background.js +131 -0
- package/dist/shell/sandbox.js +396 -0
- package/dist/telemetry/metrics.js +172 -0
- package/dist/telemetry/otlp.js +128 -0
- package/dist/telemetry/tracer.js +240 -0
- package/dist/tools/askUser.js +64 -0
- package/dist/tools/bg.js +59 -0
- package/dist/tools/common.js +158 -0
- package/dist/tools/deepResearch.js +126 -0
- package/dist/tools/diff.js +70 -0
- package/dist/tools/document/docx.js +44 -0
- package/dist/tools/document/pdf.js +190 -0
- package/dist/tools/document/pptx.js +148 -0
- package/dist/tools/document/types.js +26 -0
- package/dist/tools/document/xlsx.js +87 -0
- package/dist/tools/document.js +389 -0
- package/dist/tools/edit.js +56 -0
- package/dist/tools/fetchUrl.js +218 -0
- package/dist/tools/fuzzyMatch.js +74 -0
- package/dist/tools/glob.js +34 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/ignore.js +28 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/ls.js +27 -0
- package/dist/tools/lsp.js +239 -0
- package/dist/tools/notebook.js +215 -0
- package/dist/tools/read.js +32 -0
- package/dist/tools/repoMap.js +26 -0
- package/dist/tools/secretScan.js +143 -0
- package/dist/tools/shell.js +134 -0
- package/dist/tools/skills.js +69 -0
- package/dist/tools/subagent.js +57 -0
- package/dist/tools/tasks.js +43 -0
- package/dist/tools/webSearch.js +97 -0
- package/dist/tools/write.js +47 -0
- package/dist/tools/writeAgent.js +72 -0
- package/dist/trust/aiDisclosure.js +45 -0
- package/dist/trust/mcpTrust.js +111 -0
- package/dist/trust/trust.js +231 -0
- package/dist/types.js +1 -0
- package/dist/ui/AiDisclosurePrompt.js +23 -0
- package/dist/ui/App.js +449 -0
- package/dist/ui/Banner.js +71 -0
- package/dist/ui/ElicitationPrompt.js +46 -0
- package/dist/ui/Markdown.js +148 -0
- package/dist/ui/McpTrustPrompt.js +42 -0
- package/dist/ui/ModelPicker.js +19 -0
- package/dist/ui/PermissionPrompt.js +27 -0
- package/dist/ui/SelectList.js +24 -0
- package/dist/ui/Spinner.js +12 -0
- package/dist/ui/StatusLine.js +11 -0
- package/dist/ui/TranscriptItem.js +13 -0
- package/dist/ui/TrustPrompt.js +12 -0
- package/dist/ui/highlight.js +137 -0
- package/dist/ui/inline.js +241 -0
- package/dist/ui/mermaid.js +98 -0
- package/dist/ui/table.js +148 -0
- package/dist/ui/toolOutputPreview.js +34 -0
- package/dist/ui/useAgent.js +534 -0
- package/dist/ui/useKillSwitch.js +65 -0
- package/dist/ui/useSessionResume.js +39 -0
- package/dist/ui/useUsageBudget.js +149 -0
- package/dist/ui/viewport.js +71 -0
- package/dist/undo/undo.js +293 -0
- package/dist/version.js +4 -0
- package/package.json +122 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { matchesRule } from "./rules.js";
|
|
2
|
+
/**
|
|
3
|
+
* Permission policy. Read-only tools never prompt; mutating tools prompt
|
|
4
|
+
* unless allowed by a settings-file rule or the user chose "always allow"
|
|
5
|
+
* for that tool earlier in the session. A deny rule blocks the call outright
|
|
6
|
+
* and cannot be overridden by an allow rule or an "always" choice.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Key under which an "always allow" decision is recorded for a tool call.
|
|
10
|
+
* For `shell`, this is scoped to the *exact* command string, so approving
|
|
11
|
+
* one invocation (e.g. `python train.py`) only pre-approves that same
|
|
12
|
+
* invocation again — not every future command starting with the same
|
|
13
|
+
* program (`python -c "..."`, `python other_script.py`). Interpreters and
|
|
14
|
+
* other general-purpose runners make a first-word-only key unsafe: the
|
|
15
|
+
* first word tells you almost nothing about what the full command does.
|
|
16
|
+
* Users who want broader pre-approval on purpose can still write a wildcard
|
|
17
|
+
* rule (e.g. `shell(git *)`) into settings.json — see rules.ts.
|
|
18
|
+
*/
|
|
19
|
+
function alwaysAllowKey(toolName, args) {
|
|
20
|
+
if (toolName !== "shell")
|
|
21
|
+
return toolName;
|
|
22
|
+
const command = String(args.command ?? "").trim();
|
|
23
|
+
return command ? `shell:${command}` : "shell";
|
|
24
|
+
}
|
|
25
|
+
export class PermissionManager {
|
|
26
|
+
alwaysAllowed = new Set();
|
|
27
|
+
allow;
|
|
28
|
+
deny;
|
|
29
|
+
constructor(rules = []) {
|
|
30
|
+
if (Array.isArray(rules)) {
|
|
31
|
+
this.allow = rules;
|
|
32
|
+
this.deny = [];
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.allow = rules.allow;
|
|
36
|
+
this.deny = rules.deny;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** True if a deny rule blocks this call; such calls are never prompted, always refused. */
|
|
40
|
+
isDenied(tool, args = {}) {
|
|
41
|
+
return this.deny.some((rule) => matchesRule(rule, tool.name, args));
|
|
42
|
+
}
|
|
43
|
+
needsPrompt(tool, args = {}) {
|
|
44
|
+
if (!tool.requiresPermission)
|
|
45
|
+
return false;
|
|
46
|
+
if (this.alwaysAllowed.has(alwaysAllowKey(tool.name, args)))
|
|
47
|
+
return false;
|
|
48
|
+
return !this.allow.some((rule) => matchesRule(rule, tool.name, args));
|
|
49
|
+
}
|
|
50
|
+
record(toolName, decision, args = {}) {
|
|
51
|
+
if (decision === "always")
|
|
52
|
+
this.alwaysAllowed.add(alwaysAllowKey(toolName, args));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* True if this call is pre-approved by an earlier "always allow" choice this
|
|
56
|
+
* session (as opposed to a settings allow rule). Lets the audit log record
|
|
57
|
+
* the precise source of an allowed decision.
|
|
58
|
+
*/
|
|
59
|
+
isAlwaysAllowed(toolName, args = {}) {
|
|
60
|
+
return this.alwaysAllowed.has(alwaysAllowKey(toolName, args));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_DIR } from "../config/config.js";
|
|
4
|
+
export function loadRules(workspace, trustWorkspace = true) {
|
|
5
|
+
const allow = [];
|
|
6
|
+
const deny = [];
|
|
7
|
+
const files = [
|
|
8
|
+
{ file: path.join(CONFIG_DIR, "settings.json"), includeAllow: true },
|
|
9
|
+
{ file: path.join(workspace, ".kritya", "settings.json"), includeAllow: trustWorkspace },
|
|
10
|
+
];
|
|
11
|
+
for (const { file, includeAllow } of files) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
14
|
+
if (includeAllow && Array.isArray(parsed.allow)) {
|
|
15
|
+
allow.push(...parsed.allow.filter((r) => typeof r === "string"));
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(parsed.deny)) {
|
|
18
|
+
deny.push(...parsed.deny.filter((r) => typeof r === "string"));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Missing or malformed settings file — no rules from it.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { allow, deny };
|
|
26
|
+
}
|
|
27
|
+
const RULE_RE = /^([a-z_]+)(?:\((.*)\))?$/;
|
|
28
|
+
/** The string a pattern is matched against for a given tool. */
|
|
29
|
+
function subjectFor(toolName, args) {
|
|
30
|
+
if (toolName === "shell")
|
|
31
|
+
return String(args.command ?? "").trim();
|
|
32
|
+
return String(args.path ?? args.pattern ?? "").trim();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shell metacharacters that chain/substitute commands or redirect I/O
|
|
36
|
+
* (&&, ||, ;, |, `, $(...), ${...}, >, >>, <, <<, a bare & to background /
|
|
37
|
+
* chain, and newlines). Redirection is included because `shell(git *)` must
|
|
38
|
+
* not silently allow `git log > ~/.bashrc` or `git log < /etc/passwd` just
|
|
39
|
+
* because the allowed pattern never spelled those characters out.
|
|
40
|
+
*/
|
|
41
|
+
const SHELL_METACHAR_RE = /&&|\|\||[;|`&\n]|\$\(|\$\{|<|>/;
|
|
42
|
+
export function matchesRule(rule, toolName, args) {
|
|
43
|
+
const m = RULE_RE.exec(rule.trim());
|
|
44
|
+
if (!m)
|
|
45
|
+
return false;
|
|
46
|
+
const [, ruleTool, pattern] = m;
|
|
47
|
+
if (ruleTool !== toolName)
|
|
48
|
+
return false;
|
|
49
|
+
if (pattern === undefined)
|
|
50
|
+
return true;
|
|
51
|
+
const trimmedPattern = pattern.trim();
|
|
52
|
+
const subject = subjectFor(toolName, args);
|
|
53
|
+
// A wildcard shell(...) rule (e.g. shell(git *)) is only meant to allow one
|
|
54
|
+
// command, not an arbitrary chain appended after it. If the actual command
|
|
55
|
+
// contains shell metacharacters that the allowed pattern itself didn't
|
|
56
|
+
// spell out, refuse the match so it falls through to a permission prompt
|
|
57
|
+
// instead of silently auto-approving `git status && rm -rf /`.
|
|
58
|
+
if (toolName === "shell" &&
|
|
59
|
+
trimmedPattern.includes("*") &&
|
|
60
|
+
SHELL_METACHAR_RE.test(subject) &&
|
|
61
|
+
!SHELL_METACHAR_RE.test(trimmedPattern)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const regex = new RegExp("^" + trimmedPattern.split("*").map(escapeRegExp).join(".*") + "$");
|
|
65
|
+
return regex.test(subject);
|
|
66
|
+
}
|
|
67
|
+
function escapeRegExp(s) {
|
|
68
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
69
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Scans each root for `<name>/plugin.json` folders, in name order, and reports
|
|
6
|
+
* both what loaded and why anything didn't. Roots that don't exist are
|
|
7
|
+
* skipped silently -- an unconfigured plugins directory isn't noteworthy. A
|
|
8
|
+
* folder without a plugin.json is not a plugin and isn't reported at all. On
|
|
9
|
+
* a name collision across or within roots, the first one found wins.
|
|
10
|
+
*/
|
|
11
|
+
export function scanPluginsDetailed(roots) {
|
|
12
|
+
const seen = new Map();
|
|
13
|
+
const skipped = [];
|
|
14
|
+
for (const root of roots) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const dirs = entries
|
|
23
|
+
.filter((e) => e.isDirectory())
|
|
24
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
for (const entry of dirs) {
|
|
26
|
+
const dir = path.join(root, entry.name);
|
|
27
|
+
const manifestFile = path.join(dir, "plugin.json");
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = fs.readFileSync(manifestFile, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let manifest;
|
|
36
|
+
try {
|
|
37
|
+
manifest = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
skipped.push({ name: entry.name, dir, reason: "invalid JSON in plugin.json" });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const name = manifest.name;
|
|
44
|
+
const version = manifest.version;
|
|
45
|
+
if (typeof name !== "string" || typeof version !== "string") {
|
|
46
|
+
skipped.push({
|
|
47
|
+
name: entry.name,
|
|
48
|
+
dir,
|
|
49
|
+
reason: 'plugin.json must include "name" and "version"',
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (name !== entry.name) {
|
|
54
|
+
skipped.push({
|
|
55
|
+
name: entry.name,
|
|
56
|
+
dir,
|
|
57
|
+
reason: `folder name "${entry.name}" does not match manifest name "${name}"`,
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const existing = seen.get(name);
|
|
62
|
+
if (existing) {
|
|
63
|
+
skipped.push({
|
|
64
|
+
name: entry.name,
|
|
65
|
+
dir,
|
|
66
|
+
reason: `duplicate plugin name "${name}" (already loaded from ${existing.dir})`,
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seen.set(name, { name, dir, manifest });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { loaded: [...seen.values()], skipped };
|
|
74
|
+
}
|
|
75
|
+
let warnSink = (message) => {
|
|
76
|
+
process.stderr.write(`kritya: ${message}\n`);
|
|
77
|
+
};
|
|
78
|
+
function warn(message) {
|
|
79
|
+
warnSink(message);
|
|
80
|
+
}
|
|
81
|
+
/** For testing: override the warning sink. Returns the previous sink. */
|
|
82
|
+
export function _setWarnSink(sink) {
|
|
83
|
+
const prev = warnSink;
|
|
84
|
+
warnSink = sink;
|
|
85
|
+
return prev;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Same scan as scanPluginsDetailed, but for the common case that only wants
|
|
89
|
+
* the loaded list, warning (once per skip) about anything malformed.
|
|
90
|
+
*/
|
|
91
|
+
export function scanPlugins(roots) {
|
|
92
|
+
const { loaded, skipped } = scanPluginsDetailed(roots);
|
|
93
|
+
for (const s of skipped) {
|
|
94
|
+
warn(`skipping ${path.join(s.dir, "plugin.json")}: ${s.reason}`);
|
|
95
|
+
}
|
|
96
|
+
return loaded;
|
|
97
|
+
}
|
|
98
|
+
export function pluginsDir(workspace) {
|
|
99
|
+
return path.join(workspace, ".kritya", "plugins");
|
|
100
|
+
}
|
|
101
|
+
/** User-global plugins root, available across all workspaces. */
|
|
102
|
+
export function userPluginsDir() {
|
|
103
|
+
return path.join(os.homedir(), ".kritya", "plugins");
|
|
104
|
+
}
|
|
105
|
+
/** Maps each discovered plugin to its skills/ subfolder, for use as extra skill roots. */
|
|
106
|
+
export function pluginSkillsRoots(plugins) {
|
|
107
|
+
return plugins.map((p) => ({ dir: path.join(p.dir, "skills"), pluginName: p.name }));
|
|
108
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function isServerConfig(v) {
|
|
4
|
+
if (!v || typeof v !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const cfg = v;
|
|
7
|
+
return typeof cfg.command === "string" || typeof cfg.url === "string";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads each plugin's mcp.json (same `{ mcpServers: { name: config } }` shape
|
|
11
|
+
* as a workspace's .mcp.json). Entries requesting the legacy HTTP+SSE
|
|
12
|
+
* transport are skipped with a reason -- kritya's MCP client only speaks
|
|
13
|
+
* stdio and Streamable HTTP, and that older transport is deprecated in the
|
|
14
|
+
* spec itself.
|
|
15
|
+
*/
|
|
16
|
+
export function scanPluginMcpServers(plugins) {
|
|
17
|
+
const loaded = [];
|
|
18
|
+
const skipped = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const plugin of plugins) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(fs.readFileSync(path.join(plugin.dir, "mcp.json"), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!parsed?.mcpServers || typeof parsed.mcpServers !== "object")
|
|
29
|
+
continue;
|
|
30
|
+
for (const [name, raw] of Object.entries(parsed.mcpServers)) {
|
|
31
|
+
if (seen.has(name)) {
|
|
32
|
+
skipped.push({
|
|
33
|
+
pluginName: plugin.name,
|
|
34
|
+
name,
|
|
35
|
+
reason: `duplicate server name "${name}" (already loaded from another plugin)`,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const entry = raw;
|
|
40
|
+
if (entry?.transport === "sse" || entry?.type === "sse") {
|
|
41
|
+
skipped.push({
|
|
42
|
+
pluginName: plugin.name,
|
|
43
|
+
name,
|
|
44
|
+
reason: "legacy HTTP+SSE transport is not supported",
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!isServerConfig(raw)) {
|
|
49
|
+
skipped.push({
|
|
50
|
+
pluginName: plugin.name,
|
|
51
|
+
name,
|
|
52
|
+
reason: 'must include "command" or "url"',
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
seen.add(name);
|
|
57
|
+
loaded.push({ name, pluginName: plugin.name, cfg: raw });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { loaded, skipped };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Same scan as scanPluginMcpServers, but returns merge-ready shapes: a plain
|
|
64
|
+
* name->config map for mergeMcpServers, and a name->plugin-name provenance
|
|
65
|
+
* map for labeling `/mcp` output. Skips are reported via `warn` (defaults to
|
|
66
|
+
* stderr, like scanPlugins/scanSkills).
|
|
67
|
+
*/
|
|
68
|
+
export function loadPluginMcpServers(plugins, warn = (m) => process.stderr.write(`kritya: ${m}\n`)) {
|
|
69
|
+
const { loaded, skipped } = scanPluginMcpServers(plugins);
|
|
70
|
+
for (const s of skipped) {
|
|
71
|
+
warn(`skipping mcp server "${s.name}" from plugin "${s.pluginName}": ${s.reason}`);
|
|
72
|
+
}
|
|
73
|
+
const servers = {};
|
|
74
|
+
const provenance = {};
|
|
75
|
+
for (const s of loaded) {
|
|
76
|
+
servers[s.name] = s.cfg;
|
|
77
|
+
provenance[s.name] = s.pluginName;
|
|
78
|
+
}
|
|
79
|
+
return { servers, provenance };
|
|
80
|
+
}
|