space-architect 5.3.0 → 5.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de2f5d7d6f80acc7b6a7bd16c9ec3dbaf23ef9c2d425c8b1e532f5c24822533a
4
- data.tar.gz: b39f07d1d24b1288d54e37a08624322ad0fc4f5a664459d6bdcbe12acf4d0438
3
+ metadata.gz: 20aaf646a8056b8d5975cb789f55a49f9f9c0ca5deca38f9866a0e3e220e8904
4
+ data.tar.gz: a89104abc977aaeae263c09f922800252867df448050262c557d6f60780275f0
5
5
  SHA512:
6
- metadata.gz: 5086b34800652f32d243dce10163543a6ee182096b2a42e6938c303de99cf3fa9212591cadba7494491f04f505e16a2c569388c2434c2bbca59585ece77671a9
7
- data.tar.gz: 5824a84007e85b84a19ce50c0af0ed6318315b3060caceb5ab4e560416da098d980155bbe74833560a1f7b35330bafb2e73a25e3154f28dc08cf6f53c2ed14e8
6
+ metadata.gz: f0a104ab6b75ec7170e5c4bc7a57c4406f002e167ba13b3d913d21a97f3c020a13d3912d2f2fada42edcd0ffeac876a8fdff2fe0c38ad163f617700927767493
7
+ data.tar.gz: ea287608ae6cb4a560d3b38e51e4d8b0c3d4dc04985e429046fe83b1588c8f7dd34608a7ede28ed45457ff8e0456226d4130ddfb6b9541dde875cc3976898a1b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.4.0] - 2026-07-21
9
+
10
+ ### Changed
11
+
12
+ - **`architect sessions agent install` resolves an `op://` token at install
13
+ time** — the ref is resolved once via `op read` and the raw value is written
14
+ to the plist's `EnvironmentVariables` dict (`SPACE_ARCHITECT_INGEST_TOKEN`),
15
+ with the plist written mode 0600 and ProgramArguments carrying no token
16
+ material. Interval runs no longer invoke `op` at all, so a locked 1Password
17
+ can no longer fail the sync (previously the ref rode argv and every fire ran
18
+ `op read`). Reinstall existing agents to migrate; a non-`op://` token passes
19
+ through to the env dict unresolved.
20
+
21
+ ### Added
22
+
23
+ - **`architect sessions sync` env-token fallback** — `--token` is now
24
+ optional: when absent, the token is read from `SPACE_ARCHITECT_INGEST_TOKEN`.
25
+ An explicit `--token` always wins; with neither present, the error names
26
+ both sources.
27
+ - **Server: conversation session links** — importers preserve a session file's
28
+ own `session_id` and record a differing content-derived id as
29
+ `parent_session_id`; conversations resolve `parent`/`children` links
30
+ (owner-scoped) and Conversations/Show renders "part of" / "Subagent
31
+ transcripts" navigation between them.
32
+ - **Server: denormalized `turns_count` on conversations** — the conversations
33
+ index reads a counter column recomputed at import instead of loading every
34
+ conversation's messages, fixing index-page timeouts (502s) at
35
+ thousand-conversation scale.
36
+
8
37
  ## [5.3.0] - 2026-07-20
9
38
 
10
39
  ### Added
@@ -950,13 +950,13 @@ module Space::Architect
950
950
  # explicitly rather than relying on dry-cli's `required:` option flag.
951
951
  def self.require_credentials!(host, token)
952
952
  raise Space::Core::Error, "--host is required" unless host
953
- raise Space::Core::Error, "--token is required" unless token
953
+ raise Space::Core::Error, "--token or #{SessionSync::TOKEN_ENV} is required" unless token
954
954
  end
955
955
 
956
956
  class Sync < BaseCommand
957
957
  desc "Scan pi/claude session files and upload new/grown conversations to a space-server"
958
958
  option :host, required: true, desc: "Base URL of the space-server"
959
- option :token, required: true, desc: "Bearer token for authorization (an op:// ref is resolved once via `op read`)"
959
+ option :token, default: nil, desc: "Bearer token for authorization (defaults to $#{SessionSync::TOKEN_ENV}; an explicit --token wins; an op:// ref is resolved once via `op read`)"
960
960
  option :state_file, default: nil, desc: "Cursor YAML path (default: $XDG_STATE_HOME/space-architect/session-sync.yaml)"
961
961
  option :pi_root, default: nil, desc: "Override the pi sessions root (default: ~/.pi/agent/sessions)"
962
962
  option :claude_root, default: nil, desc: "Override the claude projects root (default: ~/.claude/projects)"
@@ -965,9 +965,10 @@ module Space::Architect
965
965
  def call(host: nil, token: nil, state_file: nil, pi_root: nil, claude_root: nil, dry_run: false, **opts)
966
966
  setup_terminal(**opts.slice(:color, :colors))
967
967
  handle_errors do
968
- Sessions.require_credentials!(host, token)
968
+ resolved_token = token || ENV[SessionSync::TOKEN_ENV]
969
+ Sessions.require_credentials!(host, resolved_token)
969
970
  runner = SessionSync::Runner.new(
970
- client: ConversationsClient.new(host, token),
971
+ client: ConversationsClient.new(host, resolved_token),
971
972
  state_path: state_file || SessionSync.default_state_file,
972
973
  pi_root: pi_root || SessionSync.default_pi_root,
973
974
  claude_root: claude_root || SessionSync.default_claude_root,
@@ -997,13 +998,14 @@ module Space::Architect
997
998
  include Helpers
998
999
  desc "Install the launchd agent that periodically runs `architect sessions sync`"
999
1000
  option :host, required: true, desc: "Base URL of the space-server"
1000
- option :token, required: true, desc: "Bearer token for authorization (an op:// ref is stored as-is in the plist)"
1001
+ option :token, required: true, desc: "Bearer token for authorization (an op:// ref is resolved once at install time; the raw value is stored in the plist's EnvironmentVariables, not argv)"
1001
1002
  option :interval, default: "900", desc: "StartInterval in seconds (default 900)"
1002
1003
 
1003
1004
  def call(host: nil, token: nil, interval: "900", **opts)
1004
1005
  setup_terminal(**opts.slice(:color, :colors))
1005
1006
  handle_errors do
1006
1007
  Sessions.require_credentials!(host, token)
1008
+ resolved_token = token.start_with?("op://") ? SessionSync.resolve_token(token) : token
1007
1009
  pp = plist_path
1008
1010
  xml = SessionSync::Plist.call(
1009
1011
  label: SessionSync::LABEL,
@@ -1011,10 +1013,11 @@ module Space::Architect
1011
1013
  log_dir: SessionSync.log_dir,
1012
1014
  bin_path: SessionSync::BinPath.detect,
1013
1015
  host: host,
1014
- token: token
1016
+ env: {SessionSync::TOKEN_ENV => resolved_token}
1015
1017
  )
1016
1018
  FileUtils.mkdir_p(File.dirname(pp))
1017
1019
  File.write(pp, xml)
1020
+ File.chmod(0o600, pp)
1018
1021
 
1019
1022
  result = Space::Src::Launchd::Agent.new(label: SessionSync::LABEL).install(pp)
1020
1023
  raise Space::Core::Error, "bootstrap failed: #{format_failure(result.failure)}" if result.failure?
@@ -16,7 +16,7 @@ module Space::Architect
16
16
  @host = host.chomp("/")
17
17
  @token = token
18
18
  @client = client
19
- @op_resolver = op_resolver || method(:op_read)
19
+ @op_resolver = op_resolver || SessionSync.method(:resolve_token)
20
20
  @resolved_token = nil
21
21
  end
22
22
 
@@ -62,13 +62,6 @@ module Space::Architect
62
62
  @resolved_token ||= @token.start_with?("op://") ? @op_resolver.call(@token) : @token
63
63
  end
64
64
 
65
- def op_read(ref)
66
- value = IO.popen(["op", "read", ref], &:read)
67
- raise Space::Core::Error, "op read failed for #{ref}" unless $?.success?
68
-
69
- value.chomp
70
- end
71
-
72
65
  def build_multipart(boundary, path, session_id)
73
66
  crlf = "\r\n"
74
67
  filename = File.basename(path)
@@ -23,10 +23,11 @@ module Space::Architect
23
23
  # @param log_dir [String] Absolute directory for stdout/stderr logs.
24
24
  # @param bin_path [String] Absolute path to the architect bin script.
25
25
  # @param host [String] --host value passed to `sessions sync`.
26
- # @param token [String] --token value passed to `sessions sync` (an op:// ref
27
- # is passed through verbatim never resolved here).
26
+ # @param env [Hash] EnvironmentVariables dict; must contain a non-empty
27
+ # SessionSync::TOKEN_ENV value (resolved by the caller
28
+ # never resolved here).
28
29
  # @return [String] the full plist XML.
29
- def call(label:, refresh_interval:, log_dir:, bin_path:, host:, token:)
30
+ def call(label:, refresh_interval:, log_dir:, bin_path:, host:, env:)
30
31
  raise ArgumentError, "label is required" if label.to_s.empty?
31
32
  raise ArgumentError, "refresh_interval must be > 0" unless refresh_interval.is_a?(Integer) && refresh_interval > 0
32
33
  %w[log_dir bin_path].each do |k|
@@ -34,7 +35,7 @@ module Space::Architect
34
35
  raise ArgumentError, "#{k} must be absolute (got #{v.inspect})" unless v.is_a?(String) && File.absolute_path?(v)
35
36
  end
36
37
  raise ArgumentError, "host is required" if host.to_s.empty?
37
- raise ArgumentError, "token is required" if token.to_s.empty?
38
+ raise ArgumentError, "env[#{SessionSync::TOKEN_ENV}] is required" if env.to_h[SessionSync::TOKEN_ENV].to_s.empty?
38
39
 
39
40
  out_log = File.join(log_dir, "#{label}.out.log")
40
41
  err_log = File.join(log_dir, "#{label}.err.log")
@@ -45,9 +46,9 @@ module Space::Architect
45
46
  bin_path,
46
47
  "sessions",
47
48
  "sync",
48
- "--host", host,
49
- "--token", token
49
+ "--host", host
50
50
  ])
51
+ body << key("EnvironmentVariables") << "\n" << dict(env)
51
52
  body << key("StartInterval") << integer(refresh_interval) << "\n"
52
53
  body << key("RunAtLoad") << boolean(true) << "\n"
53
54
  body << key("ProcessType") << string("Background") << "\n"
@@ -89,6 +90,15 @@ module Space::Architect
89
90
  items.each { |arg| out << " <string>#{escape(arg)}</string>\n" }
90
91
  out << " </array>"
91
92
  end
93
+
94
+ def dict(hash)
95
+ out = +" <dict>\n"
96
+ hash.each do |k, v|
97
+ out << " <key>#{escape(k)}</key>\n"
98
+ out << " <string>#{escape(v)}</string>\n"
99
+ end
100
+ out << " </dict>"
101
+ end
92
102
  end
93
103
  end
94
104
  end
@@ -15,9 +15,20 @@ module Space::Architect
15
15
  module SessionSync
16
16
  LABEL = "io.github.jetpks.space-architect.session-sync"
17
17
  APP_NAME = "space-architect"
18
+ TOKEN_ENV = "SPACE_ARCHITECT_INGEST_TOKEN"
18
19
 
19
20
  module_function
20
21
 
22
+ # The one op-read seam under lib/: resolves an op:// ref via `op read`.
23
+ # Shared by ConversationsClient's default op_resolver and
24
+ # Sessions::Agent::Install (both test-injectable around this call).
25
+ def resolve_token(ref)
26
+ value = IO.popen(["op", "read", ref], &:read)
27
+ raise Space::Core::Error, "op read failed for #{ref}" unless $?.success?
28
+
29
+ value.chomp
30
+ end
31
+
21
32
  def state_dir(env: ENV)
22
33
  File.join(Space::Core::XDG.state_home(env: env).to_s, APP_NAME)
23
34
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Space
4
4
  module Core
5
- VERSION = "5.3.0"
5
+ VERSION = "5.4.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: space-architect
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Jacobs