claude-inbox 0.12.1 → 0.12.3
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.
- checksums.yaml +4 -4
- data/lib/claude_inbox/settings.rb +23 -8
- data/lib/claude_inbox/slash_commands.rb +9 -7
- data/lib/claude_inbox/store/entry.rb +3 -6
- data/lib/claude_inbox/store/row.rb +3 -8
- data/lib/claude_inbox/subprocess.rb +16 -4
- data/lib/claude_inbox.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58507c555e4ab15701ccf14104a7d365abed00c4f3d4386afba2667b12a6afe1
|
|
4
|
+
data.tar.gz: 21cb5cc8697d843dae5c557b97872ccca09c0148c1c4655bcb8772b15e156d00
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24352714cad8d1974cfe056ed606027aaeac4003e621abb6fc72c76fc9c18dfad052f86ba1bd157dec43cad09415f12db64374aae836d948687eabac645145ea
|
|
7
|
+
data.tar.gz: 7e426498a2aca8f7961c9a8e8e61b68a138b8c6f2d414ac0bb5b8334f61b923966bf1894c994a76bd8880e442c43d32b8f456116ab670a4a70fbfa347bc81262
|
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module ClaudeInbox
|
|
6
|
-
# What `claude`
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# What a `claude` session in a directory gets from the settings files it
|
|
7
|
+
# reads, most specific wins: the defaults for flags left off, and the
|
|
8
|
+
# plugins it loads.
|
|
9
9
|
module Settings
|
|
10
|
+
# Strings, or nil when nothing sets them: then the CLI decides at launch.
|
|
10
11
|
Defaults = Struct.new(:model, :effort, :permission_mode, :remote)
|
|
11
12
|
|
|
12
13
|
def self.defaults(cwd, home: Dir.home)
|
|
13
|
-
user =
|
|
14
|
-
project = read(File.join(cwd, ".claude", "settings.json"))
|
|
15
|
-
local = read(File.join(cwd, ".claude", "settings.local.json"))
|
|
16
|
-
layers = [user, project, local]
|
|
14
|
+
user, project, local = layers = layers(cwd, home)
|
|
17
15
|
Defaults.new(
|
|
18
16
|
model: pick(layers, "model"),
|
|
19
17
|
effort: pick(layers, "effortLevel"),
|
|
@@ -22,6 +20,23 @@ module ClaudeInbox
|
|
|
22
20
|
)
|
|
23
21
|
end
|
|
24
22
|
|
|
23
|
+
# The "name@marketplace" keys of the plugins a session in cwd loads.
|
|
24
|
+
# Its install scope plays no part; see docs/cli-quirks.md.
|
|
25
|
+
def self.enabled_plugins(cwd, home: Dir.home)
|
|
26
|
+
layers(cwd, home).each_with_object({}) do |h, on|
|
|
27
|
+
plugins = h["enabledPlugins"]
|
|
28
|
+
on.merge!(plugins) if plugins.is_a?(Hash)
|
|
29
|
+
end.select { |_, v| v == true }.keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.layers(cwd, home)
|
|
33
|
+
[
|
|
34
|
+
read(File.join(home, ".claude", "settings.json")),
|
|
35
|
+
read(File.join(cwd, ".claude", "settings.json")),
|
|
36
|
+
read(File.join(cwd, ".claude", "settings.local.json"))
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
25
40
|
# "Enable Remote Control for all sessions" in /config. The CLI lets a
|
|
26
41
|
# repo turn it off but only user scope turn it on, and still honours the
|
|
27
42
|
# copy older versions kept in ~/.claude.json.
|
|
@@ -47,6 +62,6 @@ module ClaudeInbox
|
|
|
47
62
|
end
|
|
48
63
|
nil
|
|
49
64
|
end
|
|
50
|
-
private_class_method :remote_control, :pick
|
|
65
|
+
private_class_method :layers, :remote_control, :pick
|
|
51
66
|
end
|
|
52
67
|
end
|
|
@@ -6,10 +6,11 @@ module ClaudeInbox
|
|
|
6
6
|
# The slash commands `claude` would offer at its own prompt, read from the
|
|
7
7
|
# same places it reads them: `.claude/commands/*.md` and
|
|
8
8
|
# `.claude/skills/*/SKILL.md` under the project and the home directory,
|
|
9
|
-
# the skills and commands of
|
|
10
|
-
# the skills claude.ai syncs down
|
|
11
|
-
# live inside the CLI and are not
|
|
12
|
-
# the prompt goes to `claude --bg`
|
|
9
|
+
# the skills and commands of the plugins enabled there (named
|
|
10
|
+
# `plugin:name`), and the skills claude.ai syncs down
|
|
11
|
+
# (`anthropic-skills:name`). Built-ins live inside the CLI and are not
|
|
12
|
+
# listed; typing one still works, since the prompt goes to `claude --bg`
|
|
13
|
+
# verbatim.
|
|
13
14
|
#
|
|
14
15
|
# Pure: filesystem in, plain values out. Nothing here runs `claude`.
|
|
15
16
|
module SlashCommands
|
|
@@ -27,7 +28,7 @@ module ClaudeInbox
|
|
|
27
28
|
[
|
|
28
29
|
*from_dir(File.join(cwd, ".claude")),
|
|
29
30
|
*from_dir(File.join(home, ".claude")),
|
|
30
|
-
*plugins(home),
|
|
31
|
+
*plugins(cwd, home),
|
|
31
32
|
*synced(home)
|
|
32
33
|
].uniq(&:name).sort_by(&:name)
|
|
33
34
|
end
|
|
@@ -65,9 +66,10 @@ module ClaudeInbox
|
|
|
65
66
|
|
|
66
67
|
# `installed_plugins.json` maps "name@marketplace" to where the plugin
|
|
67
68
|
# was unpacked: one entry, or a list of them, one per install scope.
|
|
68
|
-
def plugins(home)
|
|
69
|
+
def plugins(cwd, home)
|
|
69
70
|
installed = Settings.read(File.join(home, ".claude", "plugins", "installed_plugins.json"))
|
|
70
|
-
|
|
71
|
+
enabled = Settings.enabled_plugins(cwd, home:)
|
|
72
|
+
(installed["plugins"] || {}).slice(*enabled).flat_map do |key, entries|
|
|
71
73
|
plugin = key.split("@").first
|
|
72
74
|
Array(entries).filter_map { |e| e["installPath"] if e.is_a?(Hash) }.uniq.flat_map do |root|
|
|
73
75
|
from_dir(root, prefix: plugin)
|
|
@@ -50,8 +50,6 @@ module ClaudeInbox
|
|
|
50
50
|
|
|
51
51
|
def parked? = wake_at == UNTIL_WOKEN
|
|
52
52
|
|
|
53
|
-
def snoozed_at = @h["snoozed_at"]
|
|
54
|
-
|
|
55
53
|
def last_state = @h["last_state"]
|
|
56
54
|
|
|
57
55
|
def state_since = @h["state_since"]
|
|
@@ -82,6 +80,9 @@ module ClaudeInbox
|
|
|
82
80
|
def observe(session, now)
|
|
83
81
|
now_i = now.to_i
|
|
84
82
|
if last_state != session.effective_state
|
|
83
|
+
# Only a change of state wakes a snooze early, not trouble on the
|
|
84
|
+
# PR: a session blocked when you snoozed it stays snoozed.
|
|
85
|
+
@h.delete("wake_at") if last_state && session.needs_you?
|
|
85
86
|
@h["last_state"] = session.effective_state
|
|
86
87
|
@h["state_since"] = now_i
|
|
87
88
|
end
|
|
@@ -96,7 +97,6 @@ module ClaudeInbox
|
|
|
96
97
|
@h["last_seen"] = now_i
|
|
97
98
|
row = Row.new(session: session, entry: self)
|
|
98
99
|
@h.delete("wake_at") if row.woken?(now_i)
|
|
99
|
-
@h.delete("snoozed_at") unless wake_at
|
|
100
100
|
@h.delete("settled_at") unless row.hand_settled?
|
|
101
101
|
@h.delete("acknowledged_at") unless row.acknowledged?
|
|
102
102
|
@h.delete("revived_at") unless row.revived?
|
|
@@ -105,13 +105,11 @@ module ClaudeInbox
|
|
|
105
105
|
|
|
106
106
|
def snooze(choice, now)
|
|
107
107
|
@h["wake_at"] = self.class.snooze_until(choice, now)
|
|
108
|
-
@h["snoozed_at"] = now.to_i
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
# Also lifts a hand-settle and a PR settle, so `u` undoes `x` and a resolved PR alike.
|
|
112
111
|
def wake(now)
|
|
113
112
|
@h.delete("wake_at")
|
|
114
|
-
@h.delete("snoozed_at")
|
|
115
113
|
@h.delete("settled_at")
|
|
116
114
|
@h["revived_at"] = now.to_i
|
|
117
115
|
end
|
|
@@ -121,7 +119,6 @@ module ClaudeInbox
|
|
|
121
119
|
def settle(now)
|
|
122
120
|
@h["settled_at"] = now.to_i
|
|
123
121
|
@h.delete("wake_at")
|
|
124
|
-
@h.delete("snoozed_at")
|
|
125
122
|
@h.delete("revived_at")
|
|
126
123
|
end
|
|
127
124
|
|
|
@@ -32,14 +32,9 @@ module ClaudeInbox
|
|
|
32
32
|
[label, session.cwd, session.intent, session.summary].any? { |field| field.to_s.downcase.include?(q) }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
def woken?(now)
|
|
38
|
-
return false if wake_at.nil?
|
|
39
|
-
return true if session.needs_you? && entry.state_since.to_i > entry.snoozed_at.to_i
|
|
40
|
-
return false if parked?
|
|
41
|
-
wake_at.to_i <= now.to_i
|
|
42
|
-
end
|
|
35
|
+
# Its timer has run out. Becoming blocked or failed wakes it sooner,
|
|
36
|
+
# but only Entry#observe sees that change happen.
|
|
37
|
+
def woken?(now) = !wake_at.nil? && !parked? && wake_at.to_i <= now.to_i
|
|
43
38
|
|
|
44
39
|
def snoozed?(now) = wake_at && !woken?(now)
|
|
45
40
|
|
|
@@ -10,15 +10,27 @@ module ClaudeInbox
|
|
|
10
10
|
def success? = status.success?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# `asdf exec`, which the ruby shim runs us through, puts the ruby it
|
|
14
|
+
# picked ahead of every shim on PATH and names it in ASDF_INSTALL_PATH.
|
|
15
|
+
# Left in place, every session runs our ruby whatever its project's
|
|
16
|
+
# .tool-versions says.
|
|
17
|
+
def self.unpinned(env)
|
|
18
|
+
install = env["ASDF_INSTALL_PATH"] or return env
|
|
19
|
+
prefix = File.join(install, "")
|
|
20
|
+
path = env.fetch("PATH", "").split(File::PATH_SEPARATOR).reject { |dir| dir.start_with?(prefix) }
|
|
21
|
+
env.reject { |k, _| k.start_with?("ASDF_INSTALL_") }.merge("PATH" => path.join(File::PATH_SEPARATOR))
|
|
22
|
+
end
|
|
23
|
+
|
|
13
24
|
# Children start in the environment from before Bundler, or every ruby a
|
|
14
|
-
# session runs, in any project, loads our Gemfile
|
|
15
|
-
# than set on ENV, which the poll
|
|
16
|
-
|
|
25
|
+
# session runs, in any project, loads our Gemfile, and from before asdf
|
|
26
|
+
# pinned our ruby. Handed to exec rather than set on ENV, which the poll
|
|
27
|
+
# and background threads share.
|
|
28
|
+
CHILD_ENV = unpinned(defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h).freeze
|
|
17
29
|
|
|
18
30
|
module_function
|
|
19
31
|
|
|
20
32
|
# argv as exec and Process.spawn take it, with CHILD_ENV in place of ours.
|
|
21
|
-
def command(*argv) =
|
|
33
|
+
def command(*argv) = [CHILD_ENV, *argv, {unsetenv_others: true}]
|
|
22
34
|
|
|
23
35
|
# `input` is written before any output is read, so it is for the few
|
|
24
36
|
# bytes a clipboard takes, not a stream.
|
data/lib/claude_inbox.rb
CHANGED