Skip to content

Commit df7dac6

Browse files
committed
feat: add auto-sync hook for shared hub entries
New check-hub-sync hook runs on UserPromptSubmit, daily throttled. If .connect.enc exists, silently syncs new entries from the hub to .context/shared/. No manual ctx connect sync needed after initial registration. Signed-off-by: Murat Parlakisik <parlakisik@gmail.com>
1 parent 2aa2844 commit df7dac6

7 files changed

Lines changed: 197 additions & 0 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package check_hub_sync
8+
9+
import (
10+
"os"
11+
12+
"github.com/spf13/cobra"
13+
)
14+
15+
// Cmd returns the check-hub-sync hook command.
16+
//
17+
// Returns:
18+
// - *cobra.Command: The hook command
19+
func Cmd() *cobra.Command {
20+
return &cobra.Command{
21+
Use: "check-hub-sync",
22+
Short: "Auto-sync shared hub entries",
23+
Hidden: true,
24+
RunE: func(
25+
cmd *cobra.Command, _ []string,
26+
) error {
27+
return Run(cmd, os.Stdin)
28+
},
29+
}
30+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
// Package check_hub_sync provides the check-hub-sync hook
8+
// for automatic shared hub synchronization on session start.
9+
//
10+
// Key exports: [Cmd].
11+
// See source files for implementation details.
12+
// Part of the internal subsystem.
13+
package check_hub_sync
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package check_hub_sync
8+
9+
import (
10+
"os"
11+
"path/filepath"
12+
13+
"github.com/spf13/cobra"
14+
15+
"github.com/ActiveMemory/ctx/internal/cli/system/core/check"
16+
"github.com/ActiveMemory/ctx/internal/cli/system/core/hubsync"
17+
"github.com/ActiveMemory/ctx/internal/cli/system/core/state"
18+
internalIo "github.com/ActiveMemory/ctx/internal/io"
19+
writeSetup "github.com/ActiveMemory/ctx/internal/write/setup"
20+
)
21+
22+
// throttleID is the daily throttle marker filename.
23+
const throttleID = "hub-sync"
24+
25+
// Run executes the check-hub-sync hook logic.
26+
//
27+
// If a hub connection config exists, syncs new entries from
28+
// the hub to .context/shared/. Throttled to once per day.
29+
// Silent when no hub is configured or no new entries.
30+
//
31+
// Parameters:
32+
// - cmd: Cobra command for output
33+
// - stdin: standard input for hook JSON
34+
//
35+
// Returns:
36+
// - error: Always nil (hook errors are non-fatal)
37+
func Run(cmd *cobra.Command, stdin *os.File) error {
38+
if !state.Initialized() {
39+
return nil
40+
}
41+
42+
_, sessionID, paused := check.Preamble(stdin)
43+
if paused {
44+
return nil
45+
}
46+
47+
if !hubsync.Connected() {
48+
return nil
49+
}
50+
51+
markerPath := filepath.Join(
52+
state.Dir(), throttleID,
53+
)
54+
if check.DailyThrottled(markerPath) {
55+
return nil
56+
}
57+
58+
msg := hubsync.Sync(sessionID)
59+
if msg != "" {
60+
writeSetup.Nudge(cmd, msg)
61+
}
62+
internalIo.TouchFile(markerPath)
63+
64+
return nil
65+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
// Package hubsync implements automatic hub sync for the
8+
// check-hub-sync hook, triggered on session start.
9+
//
10+
// Key exports: [Sync].
11+
// See source files for implementation details.
12+
// Part of the internal subsystem.
13+
package hubsync
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package hubsync
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"os"
13+
"path/filepath"
14+
15+
connectCfg "github.com/ActiveMemory/ctx/internal/cli/connect/core/config"
16+
"github.com/ActiveMemory/ctx/internal/cli/connect/core/render"
17+
"github.com/ActiveMemory/ctx/internal/hub"
18+
"github.com/ActiveMemory/ctx/internal/rc"
19+
)
20+
21+
// connectFile is the encrypted connection config filename.
22+
const connectFile = ".connect.enc"
23+
24+
// Connected reports whether a hub connection config exists.
25+
//
26+
// Returns:
27+
// - bool: true if .context/.connect.enc exists
28+
func Connected() bool {
29+
path := filepath.Join(rc.ContextDir(), connectFile)
30+
_, statErr := os.Stat(path)
31+
return statErr == nil
32+
}
33+
34+
// Sync pulls new entries from the hub and writes them to
35+
// .context/shared/. Returns the count of synced entries
36+
// and a formatted status message, or empty string if no
37+
// new entries.
38+
//
39+
// Parameters:
40+
// - sessionID: current session ID (unused, for future)
41+
//
42+
// Returns:
43+
// - string: status message or empty if nothing synced
44+
func Sync(_ string) string {
45+
cfg, loadErr := connectCfg.Load()
46+
if loadErr != nil {
47+
return ""
48+
}
49+
50+
client, dialErr := hub.NewClient(
51+
cfg.HubAddr, cfg.Token,
52+
)
53+
if dialErr != nil {
54+
return ""
55+
}
56+
defer func() { _ = client.Close() }()
57+
58+
entries, syncErr := client.Sync(
59+
context.Background(), cfg.Types, 0,
60+
)
61+
if syncErr != nil || len(entries) == 0 {
62+
return ""
63+
}
64+
65+
if writeErr := render.WriteEntries(entries); writeErr != nil {
66+
return ""
67+
}
68+
69+
return fmt.Sprintf(
70+
"Hub sync: %d shared entries updated",
71+
len(entries),
72+
)
73+
}

internal/cli/system/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
// - check-memory-drift: Memory bridge drift detection
3838
// - check-reminder: Session reminder surfacing
3939
// - check-freshness: Technology-dependent constant staleness check
40+
// - check-hub-sync: Auto-sync shared hub entries on session start
4041
// - check-backup-age: Backup staleness check (project-local)
4142
// - check-skill-discovery: One-shot mid-session skill tip nudge
4243
// - heartbeat: Token telemetry and billing threshold check

internal/cli/system/system.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_ceremony"
1919
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_context_size"
2020
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_freshness"
21+
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_hub_sync"
2122
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_journal"
2223
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_knowledge"
2324
"github.com/ActiveMemory/ctx/internal/cli/system/cmd/check_map_staleness"
@@ -72,6 +73,7 @@ func Cmd() *cobra.Command {
7273
check_ceremony.Cmd(),
7374
check_context_size.Cmd(),
7475
check_freshness.Cmd(),
76+
check_hub_sync.Cmd(),
7577
check_journal.Cmd(),
7678
check_knowledge.Cmd(),
7779
check_map_staleness.Cmd(),

0 commit comments

Comments
 (0)