ashlrcode 1.0.0 → 2.1.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 (104) hide show
  1. package/README.md +73 -16
  2. package/package.json +28 -9
  3. package/prompts/skills/commit.md +36 -0
  4. package/prompts/skills/coordinate.md +21 -0
  5. package/prompts/skills/daily-review.md +65 -0
  6. package/prompts/skills/debug.md +23 -0
  7. package/prompts/skills/deep-work.md +129 -0
  8. package/prompts/skills/explore.md +24 -0
  9. package/prompts/skills/init.md +39 -0
  10. package/prompts/skills/kairos.md +19 -0
  11. package/prompts/skills/plan.md +19 -0
  12. package/prompts/skills/polish.md +94 -0
  13. package/prompts/skills/pr.md +30 -0
  14. package/prompts/skills/refactor.md +26 -0
  15. package/prompts/skills/resume-branch.md +27 -0
  16. package/prompts/skills/review.md +27 -0
  17. package/prompts/skills/ship.md +32 -0
  18. package/prompts/skills/simplify.md +25 -0
  19. package/prompts/skills/test.md +19 -0
  20. package/prompts/skills/verify.md +17 -0
  21. package/prompts/skills/weekly-plan.md +63 -0
  22. package/prompts/system.md +451 -0
  23. package/src/agent/away-summary.ts +138 -0
  24. package/src/agent/context.ts +6 -0
  25. package/src/agent/coordinator.ts +494 -0
  26. package/src/agent/dream.ts +149 -11
  27. package/src/agent/error-handler.ts +51 -35
  28. package/src/agent/kairos.ts +52 -4
  29. package/src/agent/loop.ts +153 -13
  30. package/src/agent/mailbox.ts +151 -0
  31. package/src/agent/model-patches.ts +28 -3
  32. package/src/agent/product-agent.ts +463 -0
  33. package/src/agent/speculation.ts +21 -18
  34. package/src/agent/sub-agent.ts +11 -1
  35. package/src/agent/system-prompt.ts +19 -0
  36. package/src/agent/tool-executor.ts +83 -3
  37. package/src/agent/verification.ts +223 -0
  38. package/src/agent/worktree-manager.ts +50 -1
  39. package/src/cli.ts +228 -36
  40. package/src/config/features.ts +8 -8
  41. package/src/config/keychain.ts +105 -0
  42. package/src/config/permissions.ts +3 -2
  43. package/src/config/settings.ts +73 -5
  44. package/src/config/upgrade-notice.ts +15 -2
  45. package/src/mcp/client.ts +392 -2
  46. package/src/mcp/manager.ts +129 -13
  47. package/src/mcp/types.ts +4 -1
  48. package/src/migrate.ts +228 -0
  49. package/src/persistence/session.ts +209 -5
  50. package/src/providers/anthropic.ts +112 -98
  51. package/src/providers/cost-tracker.ts +71 -2
  52. package/src/providers/retry.ts +2 -4
  53. package/src/providers/types.ts +5 -1
  54. package/src/providers/xai.ts +1 -0
  55. package/src/repl.tsx +514 -127
  56. package/src/setup.ts +37 -1
  57. package/src/tools/coordinate.ts +88 -0
  58. package/src/tools/grep.ts +9 -11
  59. package/src/tools/lsp.ts +44 -32
  60. package/src/tools/registry.ts +75 -9
  61. package/src/tools/send-message.ts +89 -30
  62. package/src/tools/types.ts +2 -0
  63. package/src/tools/verify.ts +88 -0
  64. package/src/tools/web-browser.ts +8 -5
  65. package/src/tools/workflow.ts +34 -10
  66. package/src/ui/AnimatedSpinner.tsx +302 -0
  67. package/src/ui/App.tsx +16 -15
  68. package/src/ui/BuddyPanel.tsx +27 -34
  69. package/src/ui/SlashInput.tsx +99 -0
  70. package/src/ui/banner.ts +10 -0
  71. package/src/ui/buddy.ts +5 -4
  72. package/src/ui/effort.ts +5 -1
  73. package/src/ui/markdown.ts +269 -88
  74. package/src/ui/message-renderer.ts +183 -35
  75. package/src/ui/quips.json +41 -0
  76. package/src/ui/speech-bubble.ts +35 -19
  77. package/src/utils/ring-buffer.ts +101 -0
  78. package/src/voice/voice-mode.ts +13 -2
  79. package/src/__tests__/branded-types.test.ts +0 -47
  80. package/src/__tests__/context.test.ts +0 -163
  81. package/src/__tests__/cost-tracker.test.ts +0 -274
  82. package/src/__tests__/cron.test.ts +0 -197
  83. package/src/__tests__/dream.test.ts +0 -204
  84. package/src/__tests__/error-handler.test.ts +0 -192
  85. package/src/__tests__/features.test.ts +0 -69
  86. package/src/__tests__/file-history.test.ts +0 -177
  87. package/src/__tests__/hooks.test.ts +0 -145
  88. package/src/__tests__/keybindings.test.ts +0 -159
  89. package/src/__tests__/model-patches.test.ts +0 -82
  90. package/src/__tests__/permissions-rules.test.ts +0 -121
  91. package/src/__tests__/permissions.test.ts +0 -108
  92. package/src/__tests__/project-config.test.ts +0 -63
  93. package/src/__tests__/retry.test.ts +0 -321
  94. package/src/__tests__/router.test.ts +0 -158
  95. package/src/__tests__/session-compact.test.ts +0 -191
  96. package/src/__tests__/session.test.ts +0 -145
  97. package/src/__tests__/skill-registry.test.ts +0 -130
  98. package/src/__tests__/speculation.test.ts +0 -196
  99. package/src/__tests__/tasks-v2.test.ts +0 -267
  100. package/src/__tests__/telemetry.test.ts +0 -149
  101. package/src/__tests__/tool-executor.test.ts +0 -141
  102. package/src/__tests__/tool-registry.test.ts +0 -166
  103. package/src/__tests__/undercover.test.ts +0 -93
  104. package/src/__tests__/workflow.test.ts +0 -195
@@ -1,145 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import { runPreToolHooks, runPostToolHooks } from "../config/hooks.ts";
3
- import type { HooksConfig, HookDefinition } from "../config/hooks.ts";
4
-
5
- describe("runPreToolHooks", () => {
6
- test("returns allow when no hooks are configured", async () => {
7
- const result = await runPreToolHooks({}, "Bash", { command: "ls" });
8
- expect(result.action).toBe("allow");
9
- });
10
-
11
- test("returns allow when no hooks match", async () => {
12
- const hooks: HooksConfig = {
13
- preToolUse: [{ toolName: "Write", action: "deny" }],
14
- };
15
- const result = await runPreToolHooks(hooks, "Bash", {});
16
- expect(result.action).toBe("allow");
17
- });
18
-
19
- test("denies when exact toolName matches deny hook", async () => {
20
- const hooks: HooksConfig = {
21
- preToolUse: [{ toolName: "Bash", action: "deny", message: "no bash" }],
22
- };
23
- const result = await runPreToolHooks(hooks, "Bash", {});
24
- expect(result.action).toBe("deny");
25
- expect(result.message).toBe("no bash");
26
- });
27
-
28
- test("allows when exact toolName matches allow hook", async () => {
29
- const hooks: HooksConfig = {
30
- preToolUse: [{ toolName: "Read", action: "allow" }],
31
- };
32
- const result = await runPreToolHooks(hooks, "Read", {});
33
- expect(result.action).toBe("allow");
34
- });
35
-
36
- test("matches glob patterns in toolName", async () => {
37
- const hooks: HooksConfig = {
38
- preToolUse: [{ toolName: "Bash*", action: "deny", message: "no bash" }],
39
- };
40
- const result = await runPreToolHooks(hooks, "BashTool", {});
41
- expect(result.action).toBe("deny");
42
- });
43
-
44
- test("matches wildcard * to any tool", async () => {
45
- const hooks: HooksConfig = {
46
- preToolUse: [{ toolName: "*", action: "deny", message: "all blocked" }],
47
- };
48
- const result = await runPreToolHooks(hooks, "AnyTool", {});
49
- expect(result.action).toBe("deny");
50
- expect(result.message).toBe("all blocked");
51
- });
52
-
53
- test("matches inputPattern against serialized input", async () => {
54
- const hooks: HooksConfig = {
55
- preToolUse: [
56
- {
57
- inputPattern: "rm\\s+-rf",
58
- action: "deny",
59
- message: "dangerous command",
60
- },
61
- ],
62
- };
63
- const result = await runPreToolHooks(hooks, "Bash", {
64
- command: "rm -rf /",
65
- });
66
- expect(result.action).toBe("deny");
67
- });
68
-
69
- test("does not match inputPattern when pattern is absent from input", async () => {
70
- const hooks: HooksConfig = {
71
- preToolUse: [{ inputPattern: "rm\\s+-rf", action: "deny" }],
72
- };
73
- const result = await runPreToolHooks(hooks, "Bash", { command: "ls -la" });
74
- expect(result.action).toBe("allow");
75
- });
76
-
77
- test("matches when both toolName and inputPattern match", async () => {
78
- const hooks: HooksConfig = {
79
- preToolUse: [
80
- {
81
- toolName: "Bash",
82
- inputPattern: "sudo",
83
- action: "deny",
84
- message: "no sudo",
85
- },
86
- ],
87
- };
88
- // Both match
89
- const result = await runPreToolHooks(hooks, "Bash", {
90
- command: "sudo rm foo",
91
- });
92
- expect(result.action).toBe("deny");
93
-
94
- // toolName matches but inputPattern doesn't
95
- const result2 = await runPreToolHooks(hooks, "Bash", { command: "ls" });
96
- expect(result2.action).toBe("allow");
97
- });
98
-
99
- test("hook with no toolName or inputPattern matches everything", async () => {
100
- const hooks: HooksConfig = {
101
- preToolUse: [{ action: "deny", message: "global deny" }],
102
- };
103
- const result = await runPreToolHooks(hooks, "AnyTool", {});
104
- expect(result.action).toBe("deny");
105
- });
106
-
107
- test("runs command-based hook and denies on non-zero exit", async () => {
108
- const hooks: HooksConfig = {
109
- preToolUse: [{ toolName: "Bash", command: "exit 1" }],
110
- };
111
- const result = await runPreToolHooks(hooks, "Bash", {});
112
- expect(result.action).toBe("deny");
113
- });
114
-
115
- test("runs command-based hook and allows on zero exit", async () => {
116
- const hooks: HooksConfig = {
117
- preToolUse: [{ toolName: "Bash", command: "exit 0" }],
118
- };
119
- const result = await runPreToolHooks(hooks, "Bash", {});
120
- expect(result.action).toBe("allow");
121
- });
122
- });
123
-
124
- describe("runPostToolHooks", () => {
125
- test("runs without error with no hooks", async () => {
126
- // Should not throw
127
- await runPostToolHooks({}, "Bash", {}, "output");
128
- });
129
-
130
- test("runs without error with matching hook", async () => {
131
- const hooks: HooksConfig = {
132
- postToolUse: [{ toolName: "Bash", command: "true" }],
133
- };
134
- // Fire and forget, should not throw
135
- await runPostToolHooks(hooks, "Bash", {}, "output");
136
- });
137
-
138
- test("does not run hooks that don't match", async () => {
139
- const hooks: HooksConfig = {
140
- postToolUse: [{ toolName: "Write", command: "exit 1" }],
141
- };
142
- // Should not throw because the hook shouldn't match "Bash"
143
- await runPostToolHooks(hooks, "Bash", {}, "output");
144
- });
145
- });
@@ -1,159 +0,0 @@
1
- import { test, expect, describe, beforeEach } from "bun:test";
2
- import {
3
- getAction,
4
- getBindings,
5
- setBinding,
6
- resetBindings,
7
- InputHistory,
8
- } from "../ui/keybindings.ts";
9
-
10
- beforeEach(() => {
11
- resetBindings();
12
- });
13
-
14
- describe("default bindings", () => {
15
- test("default bindings are loaded", () => {
16
- const bindings = getBindings();
17
- expect(bindings.length).toBeGreaterThan(0);
18
-
19
- const actions = bindings.map((b) => b.action);
20
- expect(actions).toContain("exit");
21
- expect(actions).toContain("mode-switch");
22
- expect(actions).toContain("autocomplete");
23
- expect(actions).toContain("clear-screen");
24
- expect(actions).toContain("undo");
25
- });
26
- });
27
-
28
- describe("getAction", () => {
29
- test("matches ctrl+c to exit", () => {
30
- // ctrl+c: key="c", ctrl=true, shift=false, meta=false
31
- const action = getAction("c", true, false, false);
32
- expect(action).toBe("exit");
33
- });
34
-
35
- test("matches shift+tab to mode-switch", () => {
36
- // shift+tab: key="tab", ctrl=false, shift=true, meta=false
37
- const action = getAction("tab", false, true, false);
38
- expect(action).toBe("mode-switch");
39
- });
40
-
41
- test("matches ctrl+l to clear-screen", () => {
42
- const action = getAction("l", true, false, false);
43
- expect(action).toBe("clear-screen");
44
- });
45
-
46
- test("returns null for unbound key", () => {
47
- const action = getAction("q", true, true, true);
48
- expect(action).toBeNull();
49
- });
50
-
51
- test("matches plain arrow keys", () => {
52
- expect(getAction("up", false, false, false)).toBe("history-prev");
53
- expect(getAction("down", false, false, false)).toBe("history-next");
54
- });
55
- });
56
-
57
- describe("setBinding", () => {
58
- test("overrides an existing binding", () => {
59
- // Change exit from ctrl+c to ctrl+q
60
- setBinding("exit", "ctrl+q");
61
-
62
- // Old combo should not match
63
- expect(getAction("c", true, false, false)).toBeNull();
64
- // New combo should match
65
- expect(getAction("q", true, false, false)).toBe("exit");
66
- });
67
-
68
- test("adds a new binding", () => {
69
- setBinding("custom-action", "ctrl+shift+x");
70
- const action = getAction("x", true, true, false);
71
- expect(action).toBe("custom-action");
72
- });
73
- });
74
-
75
- describe("InputHistory", () => {
76
- test("push and prev navigation", () => {
77
- const history = new InputHistory();
78
- history.push("first");
79
- history.push("second");
80
- history.push("third");
81
-
82
- // prev goes from most recent backwards
83
- expect(history.prev("")).toBe("third");
84
- expect(history.prev("")).toBe("second");
85
- expect(history.prev("")).toBe("first");
86
- });
87
-
88
- test("prev returns null on empty history", () => {
89
- const history = new InputHistory();
90
- expect(history.prev("")).toBeNull();
91
- });
92
-
93
- test("next navigates forward", () => {
94
- const history = new InputHistory();
95
- history.push("first");
96
- history.push("second");
97
- history.push("third");
98
-
99
- // Navigate to the beginning
100
- history.prev("");
101
- history.prev("");
102
- history.prev("");
103
-
104
- // Navigate forward
105
- expect(history.next()).toBe("second");
106
- expect(history.next()).toBe("third");
107
- });
108
-
109
- test("next returns empty string when past end (clears input)", () => {
110
- const history = new InputHistory();
111
- history.push("hello");
112
-
113
- history.prev(""); // go to "hello"
114
- expect(history.next()).toBe(""); // past the end clears input
115
- });
116
-
117
- test("next returns null when not navigating", () => {
118
- const history = new InputHistory();
119
- history.push("hello");
120
- // Without calling prev first, index is -1
121
- expect(history.next()).toBeNull();
122
- });
123
-
124
- test("push resets navigation index", () => {
125
- const history = new InputHistory();
126
- history.push("first");
127
- history.push("second");
128
-
129
- // Navigate back
130
- history.prev("");
131
- expect(history.prev("")).toBe("first");
132
-
133
- // Push new entry resets index
134
- history.push("third");
135
- expect(history.prev("")).toBe("third");
136
- });
137
-
138
- test("does not push duplicate consecutive entries", () => {
139
- const history = new InputHistory();
140
- history.push("same");
141
- history.push("same");
142
- history.push("same");
143
-
144
- // Only one entry should exist
145
- expect(history.prev("")).toBe("same");
146
- // Going further back returns null (stays at index 0)
147
- const second = history.prev("");
148
- expect(second).toBe("same"); // stays at same since it's the only entry
149
- });
150
-
151
- test("wraps at beginning (stays at first entry)", () => {
152
- const history = new InputHistory();
153
- history.push("only");
154
-
155
- expect(history.prev("")).toBe("only");
156
- // Calling prev again stays at the first entry
157
- expect(history.prev("")).toBe("only");
158
- });
159
- });
@@ -1,82 +0,0 @@
1
- import { test, expect, describe } from "bun:test";
2
- import { getModelPatches, listPatches } from "../agent/model-patches.ts";
3
-
4
- describe("Model Patches", () => {
5
- describe("getModelPatches", () => {
6
- test("matches grok models (not grok-4-1-fast)", () => {
7
- const result = getModelPatches("grok-4");
8
- expect(result.names).toContain("Grok verbosity control");
9
- expect(result.names).not.toContain("Grok fast mode");
10
- expect(result.combinedSuffix).toContain("Be concise");
11
- });
12
-
13
- test("matches grok-4-1-fast specifically", () => {
14
- const result = getModelPatches("grok-4-1-fast");
15
- expect(result.names).toContain("Grok fast mode");
16
- // The generic grok pattern uses negative lookahead to exclude grok-4-1-fast
17
- expect(result.names).not.toContain("Grok verbosity control");
18
- expect(result.combinedSuffix).toContain("fast mode");
19
- });
20
-
21
- test("matches claude sonnet", () => {
22
- const result = getModelPatches("claude-3.5-sonnet");
23
- expect(result.names).toContain("Sonnet conciseness");
24
- expect(result.combinedSuffix).toContain("concise");
25
- });
26
-
27
- test("matches claude opus", () => {
28
- const result = getModelPatches("claude-opus-4");
29
- expect(result.names).toContain("Opus thoroughness");
30
- expect(result.combinedSuffix).toContain("thorough");
31
- });
32
-
33
- test("returns empty for unknown model", () => {
34
- const result = getModelPatches("some-random-model-v1");
35
- expect(result.names).toEqual([]);
36
- expect(result.combinedSuffix).toBe("");
37
- });
38
-
39
- test("anchored patterns don't match substrings incorrectly", () => {
40
- // "grok" pattern is anchored with ^, so "my-grok" shouldn't match
41
- const result = getModelPatches("my-grok-variant");
42
- expect(result.names).not.toContain("Grok verbosity control");
43
- expect(result.names).not.toContain("Grok fast mode");
44
-
45
- // "claude" pattern is anchored with ^, so "not-claude-sonnet" shouldn't match
46
- const result2 = getModelPatches("not-claude-sonnet");
47
- expect(result2.names).not.toContain("Sonnet conciseness");
48
-
49
- // "deepseek" anchored
50
- const result3 = getModelPatches("my-deepseek");
51
- expect(result3.names).not.toContain("DeepSeek format control");
52
- });
53
-
54
- test("matches case-insensitively", () => {
55
- const result = getModelPatches("Claude-3.5-Sonnet");
56
- expect(result.names).toContain("Sonnet conciseness");
57
- });
58
- });
59
-
60
- describe("listPatches", () => {
61
- test("returns all patches", () => {
62
- const patches = listPatches();
63
- expect(patches.length).toBeGreaterThanOrEqual(7);
64
-
65
- const names = patches.map((p) => p.name);
66
- expect(names).toContain("Grok verbosity control");
67
- expect(names).toContain("Grok fast mode");
68
- expect(names).toContain("Sonnet conciseness");
69
- expect(names).toContain("Opus thoroughness");
70
- expect(names).toContain("OpenAI reasoning");
71
- expect(names).toContain("DeepSeek format control");
72
- expect(names).toContain("Local model constraints");
73
- });
74
-
75
- test("returns a copy (not the internal array)", () => {
76
- const a = listPatches();
77
- const b = listPatches();
78
- expect(a).not.toBe(b);
79
- expect(a).toEqual(b);
80
- });
81
- });
82
- });
@@ -1,121 +0,0 @@
1
- import { test, expect, describe, beforeEach } from "bun:test";
2
- import {
3
- checkRules,
4
- setRules,
5
- getRules,
6
- resetPermissionsForTests,
7
- type PermissionRule,
8
- } from "../config/permissions.ts";
9
-
10
- beforeEach(() => {
11
- resetPermissionsForTests();
12
- });
13
-
14
- describe("matchesToolPattern (via checkRules)", () => {
15
- // matchesToolPattern is internal, so we test it through checkRules.
16
-
17
- test("exact match", () => {
18
- setRules([{ tool: "Bash", action: "deny" }]);
19
- expect(checkRules("Bash")).toBe("deny");
20
- expect(checkRules("Read")).toBeNull();
21
- });
22
-
23
- test("prefix glob (e.g. 'File*')", () => {
24
- setRules([{ tool: "File*", action: "allow" }]);
25
- expect(checkRules("FileRead")).toBe("allow");
26
- expect(checkRules("FileWrite")).toBe("allow");
27
- expect(checkRules("Bash")).toBeNull();
28
- });
29
-
30
- test("suffix glob (e.g. '*Bash')", () => {
31
- setRules([{ tool: "*Bash", action: "deny" }]);
32
- expect(checkRules("DangerousBash")).toBe("deny");
33
- expect(checkRules("Bash")).toBe("deny");
34
- expect(checkRules("Read")).toBeNull();
35
- });
36
-
37
- test("wildcard '*' matches everything", () => {
38
- setRules([{ tool: "*", action: "ask" }]);
39
- expect(checkRules("Bash")).toBe("ask");
40
- expect(checkRules("Read")).toBe("ask");
41
- expect(checkRules("AnythingAtAll")).toBe("ask");
42
- });
43
- });
44
-
45
- describe("checkRules", () => {
46
- test("matches tool name + inputPattern", () => {
47
- setRules([
48
- { tool: "Bash", inputPattern: "rm\\s+-rf", action: "deny" },
49
- ]);
50
- expect(checkRules("Bash", { command: "rm -rf /" })).toBe("deny");
51
- });
52
-
53
- test("returns null when no rule matches", () => {
54
- setRules([{ tool: "Bash", action: "deny" }]);
55
- expect(checkRules("Write")).toBeNull();
56
- });
57
-
58
- test("returns null when tool matches but inputPattern does not", () => {
59
- setRules([
60
- { tool: "Bash", inputPattern: "sudo", action: "deny" },
61
- ]);
62
- expect(checkRules("Bash", { command: "ls -la" })).toBeNull();
63
- });
64
-
65
- test("matches with regex inputPattern", () => {
66
- setRules([
67
- { tool: "Bash", inputPattern: "\\bsudo\\b", action: "deny" },
68
- ]);
69
- expect(checkRules("Bash", { command: "sudo apt install" })).toBe("deny");
70
- expect(checkRules("Bash", { command: "pseudocode" })).toBeNull();
71
- });
72
-
73
- test("first matching rule wins", () => {
74
- setRules([
75
- { tool: "Bash", inputPattern: "sudo", action: "deny" },
76
- { tool: "Bash", action: "allow" },
77
- ]);
78
- // The first rule matches sudo commands
79
- expect(checkRules("Bash", { command: "sudo rm" })).toBe("deny");
80
- // The second rule catches all other Bash
81
- expect(checkRules("Bash", { command: "ls" })).toBe("allow");
82
- });
83
-
84
- test("invalid regex in inputPattern is skipped", () => {
85
- setRules([
86
- { tool: "Bash", inputPattern: "[invalid", action: "deny" },
87
- { tool: "Bash", action: "allow" },
88
- ]);
89
- // First rule has bad regex and is skipped; second rule matches
90
- expect(checkRules("Bash", { command: "anything" })).toBe("allow");
91
- });
92
-
93
- test("no input provided — rules without inputPattern still match", () => {
94
- setRules([{ tool: "Bash", action: "deny" }]);
95
- expect(checkRules("Bash")).toBe("deny");
96
- });
97
- });
98
-
99
- describe("setRules and getRules", () => {
100
- test("setRules replaces rules and getRules returns them", () => {
101
- const rules: PermissionRule[] = [
102
- { tool: "Bash", action: "deny" },
103
- { tool: "Read", action: "allow" },
104
- ];
105
- setRules(rules);
106
- expect(getRules()).toBe(rules);
107
- });
108
-
109
- test("getRules returns empty array initially", () => {
110
- expect(getRules()).toEqual([]);
111
- });
112
- });
113
-
114
- describe("resetPermissionsForTests", () => {
115
- test("clears all rules", () => {
116
- setRules([{ tool: "Bash", action: "deny" }]);
117
- expect(getRules().length).toBe(1);
118
- resetPermissionsForTests();
119
- expect(getRules()).toEqual([]);
120
- });
121
- });
@@ -1,108 +0,0 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
- import { existsSync, mkdtempSync, rmSync } from "fs";
3
- import { join } from "path";
4
- import { tmpdir } from "os";
5
- import {
6
- checkPermission,
7
- allowForSession,
8
- getPermissionState,
9
- recordPermission,
10
- resetPermissionsForTests,
11
- } from "../config/permissions.ts";
12
- import { setConfigDirForTests } from "../config/settings.ts";
13
-
14
- let configDir: string;
15
-
16
- beforeEach(() => {
17
- configDir = mkdtempSync(join(tmpdir(), "ashlrcode-permissions-test-"));
18
- setConfigDirForTests(configDir);
19
- resetPermissionsForTests();
20
- });
21
-
22
- afterEach(() => {
23
- resetPermissionsForTests();
24
- setConfigDirForTests(null);
25
- if (existsSync(configDir)) rmSync(configDir, { recursive: true, force: true });
26
- });
27
-
28
- describe("checkPermission", () => {
29
- // Note: tests interact with module-level state. The permission module uses
30
- // a global singleton state. We test the logic as-is.
31
-
32
- test("auto-allows read-only tools", () => {
33
- expect(checkPermission("Read")).toBe("allow");
34
- expect(checkPermission("Glob")).toBe("allow");
35
- expect(checkPermission("Grep")).toBe("allow");
36
- expect(checkPermission("AskUser")).toBe("allow");
37
- expect(checkPermission("WebFetch")).toBe("allow");
38
- expect(checkPermission("Agent")).toBe("allow");
39
- });
40
-
41
- test("returns 'ask' for unknown non-read-only tools", () => {
42
- // A tool not in the auto-allow list and not in any permission set
43
- expect(checkPermission("SomeRandomTool_" + Date.now())).toBe("ask");
44
- });
45
-
46
- test("allowForSession makes tool return 'allow'", () => {
47
- const toolName = `SessionTool_${Date.now()}`;
48
- expect(checkPermission(toolName)).toBe("ask");
49
- allowForSession(toolName);
50
- expect(checkPermission(toolName)).toBe("allow");
51
- });
52
- });
53
-
54
- describe("recordPermission", () => {
55
- test("always_allow persists and changes check result", async () => {
56
- const toolName = `AlwaysAllowTool_${Date.now()}`;
57
- expect(checkPermission(toolName)).toBe("ask");
58
- await recordPermission(toolName, "always_allow");
59
- expect(checkPermission(toolName)).toBe("allow");
60
- // Clean up: remove from state
61
- const state = getPermissionState();
62
- state.alwaysAllow.delete(toolName);
63
- });
64
-
65
- test("always_deny persists and changes check result", async () => {
66
- const toolName = `AlwaysDenyTool_${Date.now()}`;
67
- await recordPermission(toolName, "always_deny");
68
- expect(checkPermission(toolName)).toBe("deny");
69
- // Clean up
70
- const state = getPermissionState();
71
- state.alwaysDeny.delete(toolName);
72
- });
73
-
74
- test("always_allow removes from alwaysDeny", async () => {
75
- const toolName = `FlipTool_${Date.now()}`;
76
- await recordPermission(toolName, "always_deny");
77
- expect(checkPermission(toolName)).toBe("deny");
78
- await recordPermission(toolName, "always_allow");
79
- expect(checkPermission(toolName)).toBe("allow");
80
- // Clean up
81
- const state = getPermissionState();
82
- state.alwaysAllow.delete(toolName);
83
- });
84
-
85
- test("always_deny removes from alwaysAllow", async () => {
86
- const toolName = `FlipTool2_${Date.now()}`;
87
- await recordPermission(toolName, "always_allow");
88
- expect(checkPermission(toolName)).toBe("allow");
89
- await recordPermission(toolName, "always_deny");
90
- expect(checkPermission(toolName)).toBe("deny");
91
- // Clean up
92
- const state = getPermissionState();
93
- state.alwaysDeny.delete(toolName);
94
- });
95
-
96
- test("allow_once does not persist", async () => {
97
- const toolName = `OnceTool_${Date.now()}`;
98
- await recordPermission(toolName, "allow_once");
99
- // Still "ask" because allow_once doesn't persist
100
- expect(checkPermission(toolName)).toBe("ask");
101
- });
102
-
103
- test("deny_once does not persist", async () => {
104
- const toolName = `DenyOnceTool_${Date.now()}`;
105
- await recordPermission(toolName, "deny_once");
106
- expect(checkPermission(toolName)).toBe("ask");
107
- });
108
- });
@@ -1,63 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
- import { tmpdir } from "os";
5
- import { loadProjectConfig } from "../config/project-config.ts";
6
- import { setConfigDirForTests } from "../config/settings.ts";
7
-
8
- describe("loadProjectConfig", () => {
9
- let rootDir: string;
10
- let configDir: string;
11
-
12
- beforeEach(() => {
13
- rootDir = mkdtempSync(join(tmpdir(), "ashlrcode-project-config-"));
14
- configDir = join(rootDir, ".config");
15
- mkdirSync(configDir, { recursive: true });
16
- setConfigDirForTests(configDir);
17
- });
18
-
19
- afterEach(() => {
20
- setConfigDirForTests(null);
21
- if (existsSync(rootDir)) rmSync(rootDir, { recursive: true, force: true });
22
- });
23
-
24
- test("loads global, parent, and cwd instructions in increasing precedence order", async () => {
25
- const parentDir = join(rootDir, "workspace");
26
- const childDir = join(parentDir, "app");
27
- mkdirSync(childDir, { recursive: true });
28
-
29
- writeFileSync(join(configDir, "ASHLR.md"), "global rule", "utf-8");
30
- writeFileSync(join(parentDir, "ASHLR.md"), "parent rule", "utf-8");
31
- writeFileSync(join(childDir, "CLAUDE.md"), "child rule", "utf-8");
32
-
33
- const config = await loadProjectConfig(childDir);
34
-
35
- expect(config.sources).toEqual([
36
- join(configDir, "ASHLR.md"),
37
- join(parentDir, "ASHLR.md"),
38
- join(childDir, "CLAUDE.md"),
39
- ]);
40
- expect(config.instructions.indexOf("global rule")).toBeLessThan(
41
- config.instructions.indexOf("parent rule")
42
- );
43
- expect(config.instructions.indexOf("parent rule")).toBeLessThan(
44
- config.instructions.indexOf("child rule")
45
- );
46
- });
47
-
48
- test("walks ancestor directories and includes both ASHLR.md and CLAUDE.md", async () => {
49
- const parentDir = join(rootDir, "workspace");
50
- const childDir = join(parentDir, "app");
51
- mkdirSync(childDir, { recursive: true });
52
-
53
- writeFileSync(join(rootDir, "ASHLR.md"), "root rule", "utf-8");
54
- writeFileSync(join(parentDir, "CLAUDE.md"), "parent claude", "utf-8");
55
-
56
- const config = await loadProjectConfig(childDir);
57
-
58
- expect(config.sources).toContain(join(rootDir, "ASHLR.md"));
59
- expect(config.sources).toContain(join(parentDir, "CLAUDE.md"));
60
- expect(config.instructions).toContain("root rule");
61
- expect(config.instructions).toContain("parent claude");
62
- });
63
- });