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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a50d40ba0f8df6b8b53f3613a75b5982871eaa3c6f9d0c29c46411a1e70323
4
- data.tar.gz: f8abcc49a7a5d61d339189f79c0f7a62d800b98531ce1e82060efda0a91e0d2e
3
+ metadata.gz: 58507c555e4ab15701ccf14104a7d365abed00c4f3d4386afba2667b12a6afe1
4
+ data.tar.gz: 21cb5cc8697d843dae5c557b97872ccca09c0148c1c4655bcb8772b15e156d00
5
5
  SHA512:
6
- metadata.gz: 1c994b3434e3e45fbf60f6c472227fb97610ae8355bb0dd166fb49c83713025fc8be70d2f06660e4475aa0606604a6860ce063c2d721fb836c048e7f18393b86
7
- data.tar.gz: 591b90b883b64fc4494d8dec115958c618cf88ad13fd5cd3e9a125cbba6364df80b7c370d6d49c6db97bb74e65905882ede46bf9fdcab4b2201d39bc1e582d19
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` will use when a flag is left off: read from the same
7
- # settings files it reads, most specific wins. Values are strings, or
8
- # nil when nothing sets them — then the CLI decides at launch.
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 = read(File.join(home, ".claude", "settings.json"))
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 installed plugins (named `plugin:name`), and
10
- # the skills claude.ai syncs down (`anthropic-skills:name`). Built-ins
11
- # live inside the CLI and are not listed; typing one still works, since
12
- # the prompt goes to `claude --bg` verbatim.
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
- (installed["plugins"] || {}).flat_map do |key, entries|
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
- # A session already blocked when you snoozed it stays snoozed; one that
36
- # *becomes* blocked or fails afterwards wakes early.
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. Handed to exec rather
15
- # than set on ENV, which the poll and background threads share.
16
- CHILD_ENV = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env.freeze : nil
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) = CHILD_ENV ? [CHILD_ENV, *argv, {unsetenv_others: true}] : 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeInbox
4
- VERSION = "0.12.1"
4
+ VERSION = "0.12.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-inbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.12.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Byron