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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. 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
+ }