Skip to content

Commit 8183af5

Browse files
GenerQAQclaude
andauthored
feat(cli): improve auth validation, paginated responses, and UX (#425)
- Add ValidateAndRefresh to verify tokens with Supabase instead of only checking local expiry - Add `dash ping` command to verify API connectivity for current project - Fix paginated response handling with generic PaginatedResponse[T] type - Fix CreateProjectResponse/RotateKeyResponse to match actual API output - Clear credentials.json on logout alongside auth.json - Remove ACONTEXT_API_KEY/ACONTEXT_API_TOKEN env var fallbacks (use credentials.json) - Rename artifacts `ls` subcommand to `list` for consistency - Fix TUI input to handle paste (multi-rune) correctly - Add comprehensive test coverage for API client methods - Update SKILL.md docs with ping command and improved instructions Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9151ffa commit 8183af5

20 files changed

Lines changed: 1430 additions & 141 deletions

landingpage/public/SKILL.md

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ Acontext provides Agent Skills as a Memory Layer for production AI agents. It pr
2323

2424
### 1. Install Acontext CLI
2525

26+
If Acontext CLI is already installed, check for updates first:
27+
```bash
28+
acontext upgrade
29+
```
30+
31+
If not installed, install it:
2632
```bash
2733
curl -fsSL https://install.acontext.io | sh
2834
```
29-
After installation, restart your shell or run `source ~/.bashrc` (or `~/.zshrc`) to make sure the CLI is in your PATH.
3035

3136
> For system-wide installation:
3237
> ```bash
@@ -41,6 +46,7 @@ acontext login
4146
- If you're in a Interactive Terminal(TTY), this command will open a browser for OAuth, then guides you through project creation. Your API key is saved automatically.
4247
- If you're in a Non-interactive Terminal(agent/CI), this command will print a login URL for the user to open manually. After user completes, run `acontext login --poll` to finish authentication.
4348
- Set up a project via `acontext dash projects` commands. If Acontext has existing projects, make sure the user wants to use an existing project or create a new project for you.
49+
4450
### 3. Add Acontext to Your Agent
4551

4652
Both plugins automatically read your API key and user email from `~/.acontext/credentials.json` and `~/.acontext/auth.json` (written by `acontext login`). No manual configuration is needed after login.
@@ -88,18 +94,22 @@ openclaw gateway
8894
After you have logged in, you can manage Acontext projects via CLI:
8995

9096
1. `acontext dash projects list --json` — list available projects
91-
2. If user ask you to use a existing Acontext project, you should let the user to provider the api key. And then switch to this project `acontext dash projects select --project <project-id> --api-key <sk-ac-...>`.
92-
3. To create, ask for an org name and project name, then run: `acontext dash projects create --name <project-name> --org <org-id>`, this command would return the API Key to you, and then select to the new project.
97+
2. If user ask you to use a existing Acontext project, you should let the user to provide the api key. And then switch to this project `acontext dash projects select --project <project-id> --api-key <sk-ac-...>`.
98+
3. To create, ask for an org name and project name, then run: `acontext dash projects create --name <project-name> --org <org-id>`. This command returns the API key and auto-saves it as the default project (no need to run `select` afterwards).
99+
4. After select or create, verify the project is configured correctly:
100+
- Check that `~/.acontext/credentials.json` contains the project key.
101+
- Run `acontext dash ping` to verify API connectivity. A successful response confirms the project is reachable.
93102

94103
## CLI Commands Reference
95104

96105

97106
All dashboard commands are under `acontext dash`:
98107

99-
| Command Group | Subcommands | Description |
100-
| --------------- | ----------------------------------- | -------------------------------------- |
101-
| `dash projects` | `list`, `select`, `create`, `stats` | Manage projects and organizations |
102-
| `dash open` || Open the Acontext Dashboard in browser |
108+
| Command Group | Subcommands | Description |
109+
| --------------- | --------------------------------------------- | -------------------------------------- |
110+
| `dash projects` | `list`, `select`, `create`, `delete`, `stats` | Manage projects and organizations |
111+
| `dash ping` || Verify API connectivity for the current project |
112+
| `dash open` || Open the Acontext Dashboard in browser |
103113

104114
### Skill Commands
105115

@@ -118,7 +128,7 @@ The directory must contain a `SKILL.md` with name and description in YAML front-
118128
| Command | Description |
119129
| ------------------ | --------------------------------- |
120130
| `acontext login` | Log in via browser OAuth |
121-
| `acontext logout` | Clear stored credentials |
131+
| `acontext logout` | Clear stored credentials (auth.json + credentials.json) |
122132
| `acontext whoami` | Show the currently logged-in user |
123133
| `acontext version` | Show version info |
124134
| `acontext upgrade` | Upgrade CLI to latest version |
@@ -194,9 +204,10 @@ Restart your shell or run `source ~/.bashrc` / `source ~/.zshrc`. The installer
194204

195205
### API returns 401 Unauthorized
196206

197-
- Verify your API key with `acontext whoami`
198-
- Re-login with `acontext login`
199-
- For CI/CD, ensure `ACONTEXT_API_KEY` is set correctly
207+
- Run `acontext dash ping` to check if the current project key is valid
208+
- If ping fails, re-select with a valid API key: `acontext dash projects select --project <id> --api-key <sk-ac-...>`
209+
- Verify login status with `acontext whoami`
210+
- Re-login with `acontext login` if needed
200211

201212
### Claude Code plugin not working
202213

src/client/acontext-cli/cmd/dash.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"fmt"
5-
"os"
65

76
"github.com/memodb-io/Acontext/acontext-cli/internal/api"
87
"github.com/memodb-io/Acontext/acontext-cli/internal/auth"
@@ -45,11 +44,9 @@ var DashCmd = &cobra.Command{
4544
if err != nil || af == nil {
4645
return fmt.Errorf("not logged in — run 'acontext login' first")
4746
}
48-
if af.IsExpired() {
49-
af, err = auth.RefreshIfNeeded(af)
50-
if err != nil {
51-
return fmt.Errorf("session expired — run 'acontext login' again")
52-
}
47+
af, err = auth.ValidateAndRefresh(af)
48+
if err != nil {
49+
return fmt.Errorf("session invalid — run 'acontext login' again: %w", err)
5350
}
5451
dashUserEmail = af.User.Email
5552
dashUserID = af.User.ID
@@ -58,16 +55,13 @@ var DashCmd = &cobra.Command{
5855
// 2. Admin client always available (JWT only)
5956
dashAdminClient = api.NewAdminClient(dashBaseURL, af.AccessToken)
6057

61-
// 3. Resolve API key for /api/v1 routes
58+
// 3. Resolve API key for /api/v1 routes: --api-key flag > --project flag > credentials.json default
6259
apiKey := dashAPIKey
63-
if apiKey == "" {
64-
apiKey = os.Getenv("ACONTEXT_API_KEY")
65-
}
6660
if apiKey == "" && dashProject != "" {
6761
apiKey = auth.GetProjectKey(dashProject)
6862
}
6963
if apiKey == "" {
70-
// Try default project
64+
// Try default project from credentials.json
7165
ks, _ := auth.LoadKeyStore()
7266
if ks != nil && ks.DefaultProject != "" {
7367
apiKey = ks.Keys[ks.DefaultProject]
@@ -86,7 +80,7 @@ var DashCmd = &cobra.Command{
8680
}
8781

8882
func init() {
89-
DashCmd.PersistentFlags().StringVar(&dashAPIKey, "api-key", "", "Project API key (or set ACONTEXT_API_KEY)")
83+
DashCmd.PersistentFlags().StringVar(&dashAPIKey, "api-key", "", "Project API key (overrides credentials.json)")
9084
DashCmd.PersistentFlags().StringVar(&dashProject, "project", "", "Project ID to use")
9185
DashCmd.PersistentFlags().BoolVar(&dashJSON, "json", false, "Output as JSON")
9286
DashCmd.PersistentFlags().StringVar(&dashBaseURL, "base-url", "", "API base URL override")

src/client/acontext-cli/cmd/dash_artifacts.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
func init() {
1414
artifactsCmd := &cobra.Command{Use: "artifacts", Short: "Manage artifacts within a disk"}
1515

16-
lsCmd := &cobra.Command{
17-
Use: "ls <disk-id>", Short: "List artifacts in a disk", Args: cobra.ExactArgs(1),
16+
listCmd := &cobra.Command{
17+
Use: "list <disk-id>", Short: "List artifacts in a disk", Args: cobra.ExactArgs(1),
1818
RunE: func(cmd *cobra.Command, args []string) error {
1919
c, err := requireClient()
2020
if err != nil {
@@ -85,6 +85,6 @@ func init() {
8585
}
8686
deleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
8787

88-
artifactsCmd.AddCommand(lsCmd, uploadCmd, deleteCmd)
88+
artifactsCmd.AddCommand(listCmd, uploadCmd, deleteCmd)
8989
DashCmd.AddCommand(artifactsCmd)
9090
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/memodb-io/Acontext/acontext-cli/internal/output"
7+
"github.com/memodb-io/Acontext/acontext-cli/internal/tui"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func init() {
12+
pingCmd := &cobra.Command{
13+
Use: "ping",
14+
Short: "Check connectivity to the Acontext API",
15+
Long: "Verifies that the current project's API key is valid and the API is reachable.",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
client, err := requireClient()
18+
if err != nil {
19+
return err
20+
}
21+
22+
if err := client.Ping(cmd.Context()); err != nil {
23+
if dashJSON {
24+
return output.RenderJSON(map[string]interface{}{
25+
"status": "error",
26+
"project": dashProject,
27+
"error": err.Error(),
28+
})
29+
}
30+
fmt.Printf("Ping failed for project %s: %v\n", dashProject, err)
31+
fmt.Println()
32+
fmt.Println("To fix this, re-select your project with a valid API key:")
33+
fmt.Printf(" acontext dash projects select --project %s --api-key <sk-ac-...>\n", dashProject)
34+
fmt.Println()
35+
fmt.Println("The API key can be found on the Acontext Dashboard:")
36+
fmt.Println(" https://dash.acontext.io")
37+
return fmt.Errorf("ping failed")
38+
}
39+
40+
if dashJSON {
41+
return output.RenderJSON(map[string]interface{}{
42+
"status": "ok",
43+
"project": dashProject,
44+
})
45+
}
46+
47+
fmt.Println(tui.RenderSuccess(fmt.Sprintf("Project %s is reachable. Setup complete.", dashProject)))
48+
return nil
49+
},
50+
}
51+
52+
DashCmd.AddCommand(pingCmd)
53+
}

src/client/acontext-cli/cmd/dash_projects.go

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import (
1212
"github.com/spf13/cobra"
1313
)
1414

15+
// printSetupComplete prints the common message after a project is configured.
16+
func printSetupComplete() {
17+
fmt.Println("API key saved to ~/.acontext/credentials.json")
18+
fmt.Println()
19+
fmt.Println("Verify connectivity:")
20+
fmt.Println(" acontext dash ping")
21+
}
22+
1523
func init() {
1624
projectsCmd := &cobra.Command{Use: "projects", Short: "Manage projects (requires login)"}
1725

@@ -98,9 +106,7 @@ func init() {
98106
return fmt.Errorf("save project key: %w", err)
99107
}
100108
fmt.Printf("Default project set to: %s\n", projectFlag)
101-
fmt.Println("API key saved locally.")
102-
fmt.Println()
103-
fmt.Println("Setup complete. You can now use 'acontext dash' commands.")
109+
printSetupComplete()
104110
return nil
105111
}
106112

@@ -114,9 +120,8 @@ func init() {
114120
return fmt.Errorf("rotate project key: %w", err)
115121
}
116122
fmt.Printf("Default project set to: %s\n", projectFlag)
117-
fmt.Println("New API key generated and saved locally.")
118-
fmt.Println()
119-
fmt.Println("Setup complete. You can now use 'acontext dash' commands.")
123+
fmt.Println("New API key generated.")
124+
printSetupComplete()
120125
return nil
121126
}
122127

@@ -136,9 +141,7 @@ func init() {
136141
return fmt.Errorf("save project key: %w", err)
137142
}
138143
fmt.Printf("Default project set to: %s\n", projectFlag)
139-
fmt.Println("API key saved locally.")
140-
fmt.Println()
141-
fmt.Println("Setup complete. You can now use 'acontext dash' commands.")
144+
printSetupComplete()
142145
return nil
143146
}
144147

@@ -157,7 +160,7 @@ func init() {
157160
return fmt.Errorf("save project key: %w", err)
158161
}
159162
fmt.Println(tui.RenderSuccess(fmt.Sprintf("Default project set to: %s", choice.Name)))
160-
fmt.Println(tui.RenderInfo("API key saved locally."))
163+
printSetupComplete()
161164
return nil
162165
},
163166
}
@@ -223,24 +226,22 @@ func init() {
223226
}
224227

225228
// 2. Link project to org via Supabase RPC
226-
if err := auth.LinkProjectToOrg(dashAccessToken, orgID, name, project.ID); err != nil {
229+
if err := auth.LinkProjectToOrg(dashAccessToken, orgID, name, project.ProjectID); err != nil {
227230
return fmt.Errorf("link project to organization: %w", err)
228231
}
229232

230233
if dashJSON {
231234
return output.RenderJSON(project)
232235
}
233-
fmt.Printf("Project created: %s (%s)\n", name, project.ID)
236+
fmt.Printf("Project created: %s (%s)\n", name, project.ProjectID)
234237

235238
// Auto-save API key and set as default
236239
if project.SecretKey != "" {
237-
if err := auth.SetProjectKey(project.ID, project.SecretKey); err == nil {
238-
fmt.Println("API key saved locally.")
240+
if err := auth.SetProjectKey(project.ProjectID, project.SecretKey); err == nil {
241+
_ = auth.SetDefaultProject(project.ProjectID)
242+
fmt.Printf("Default project set to: %s\n", project.ProjectID)
243+
printSetupComplete()
239244
}
240-
_ = auth.SetDefaultProject(project.ID)
241-
fmt.Printf("Default project set to: %s\n", project.ID)
242-
fmt.Println()
243-
fmt.Println("Setup complete. You can now use 'acontext dash' commands.")
244245
}
245246
return nil
246247
},
@@ -287,10 +288,8 @@ func init() {
287288
return output.RenderJSON(stats)
288289
}
289290
fmt.Printf("Sessions: %d\n", stats.SessionCount)
290-
fmt.Printf("Messages: %d\n", stats.MessageCount)
291-
fmt.Printf("Disks: %d\n", stats.DiskCount)
291+
fmt.Printf("Tasks: %d\n", stats.TaskCount)
292292
fmt.Printf("Skills: %d\n", stats.SkillCount)
293-
fmt.Printf("Users: %d\n", stats.UserCount)
294293
return nil
295294
},
296295
}

0 commit comments

Comments
 (0)