Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions lib/config-dir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2026 Blues Inc. All rights reserved.
// Use of this source code is governed by licenses granted by the copyright holder.

package lib

import (
"fmt"
"os"
"path/filepath"
"runtime"
)

// ConfigDir returns the directory holding the configuration, creating it if needed.
// It is "note" within $XDG_CONFIG_HOME, or ~/.config when that isn't set, and within
// %AppData% on Windows.
func ConfigDir() (string, error) {
base, err := configBaseDir()
if err != nil {
return "", fmt.Errorf("can't find the configuration directory: %w", err)
}
path := filepath.Join(base, "note")
if err := os.MkdirAll(path, 0700); err != nil {
return "", fmt.Errorf("can't create the configuration directory: %w", err)
}
return path, nil
}

func configBaseDir() (string, error) {
if runtime.GOOS == "windows" {
return os.UserConfigDir()
}
// A relative XDG_CONFIG_HOME is invalid and is ignored, as the XDG spec requires
if dir := os.Getenv("XDG_CONFIG_HOME"); filepath.IsAbs(dir) {
return dir, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config"), nil
}

// Get the pathname of config settings
func defaultConfigSettingsPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "config.json"), nil
}
20 changes: 15 additions & 5 deletions lib/config-refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func refreshTestConfig(t *testing.T, expires time.Time) *ConfigSettings {
oldPath, oldHub := configSettingsPath, configSavedHub
t.Cleanup(func() { configSettingsPath, configSavedHub = oldPath, oldHub })
path := filepath.Join(t.TempDir(), "config.json")
configSettingsPath = func() string { return path }
configSettingsPath = func() (string, error) { return path, nil }
configSavedHub = "api.other.example"
t.Setenv(ConfigTokenEnvVar, "")
settings := &ConfigSettings{Hub: "api.example.com", HubCreds: map[string]ConfigCreds{
Expand Down Expand Up @@ -78,7 +78,7 @@ func TestCredentialRefresh(t *testing.T) {
}
}
t.Setenv(ConfigTokenEnvVar, test.env)
before, _ := os.ReadFile(configSettingsPath())
before, _ := os.ReadFile(testConfigPath(t))
calls := 0
original := http.DefaultTransport
t.Cleanup(func() { http.DefaultTransport = original })
Expand All @@ -105,7 +105,7 @@ func TestCredentialRefresh(t *testing.T) {
if test.status == 400 && !errors.Is(err, notehub.ErrRefreshTokenRejected) {
t.Fatalf("lost rejection: %v", err)
}
after, _ := os.ReadFile(configSettingsPath())
after, _ := os.ReadFile(testConfigPath(t))
if !changed {
if !bytes.Equal(before, after) {
t.Error("changed credentials without a successful refresh")
Expand Down Expand Up @@ -237,7 +237,7 @@ func TestConfigRefreshProcess(t *testing.T) {
if path == "" {
return
}
configSettingsPath = func() string { return path }
configSettingsPath = func() (string, error) { return path, nil }
settings, err := readConfigFromFile()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -291,7 +291,7 @@ func TestConcurrentProcessRefresh(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
t.Cleanup(cancel)
c := &child{cmd: exec.CommandContext(ctx, executable, "-test.run=^TestConfigRefreshProcess$")}
c.cmd.Env = append(os.Environ(), "NOTECLI_REFRESH_TEST_PATH="+configSettingsPath(), "NOTECLI_REFRESH_TEST_URL="+server.URL)
c.cmd.Env = append(os.Environ(), "NOTECLI_REFRESH_TEST_PATH="+testConfigPath(t), "NOTECLI_REFRESH_TEST_URL="+server.URL)
c.cmd.Stderr = &c.stderr
c.stdin, err = c.cmd.StdinPipe()
if err != nil {
Expand Down Expand Up @@ -324,3 +324,13 @@ func TestConcurrentProcessRefresh(t *testing.T) {
t.Fatalf("expected one renewal, got %d", calls.Load())
}
}

// testConfigPath returns the configuration path that the test installed
func testConfigPath(t *testing.T) string {
t.Helper()
path, err := configSettingsPath()
if err != nil {
t.Fatal(err)
}
return path
}
11 changes: 9 additions & 2 deletions lib/config-storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ var configSettingsPath = defaultConfigSettingsPath
func withConfigLock(ctx context.Context, fn func() error) error {
ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
file, err := os.OpenFile(configSettingsPath()+".lock", os.O_CREATE|os.O_RDWR, 0600)
path, err := configSettingsPath()
if err != nil {
return err
}
file, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return fmt.Errorf("locking configuration: %w", err)
}
Expand All @@ -44,7 +48,10 @@ func withConfigLock(ctx context.Context, fn func() error) error {
// writeConfigJSON replaces a complete file atomically, so readers never see half
// of a rotated token pair. The caller holds the configuration lock.
func writeConfigJSON(contents []byte) error {
path := configSettingsPath()
path, err := configSettingsPath()
if err != nil {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".config-*")
if err != nil {
return fmt.Errorf("saving configuration: %w", err)
Expand Down
29 changes: 0 additions & 29 deletions lib/config-unix.go

This file was deleted.

29 changes: 0 additions & 29 deletions lib/config-windows.go

This file was deleted.

5 changes: 4 additions & 1 deletion lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,10 @@ func defaultConfig() *ConfigSettings {
// returns (nil, non-nil) if there was some any other error
func readConfigFromFile() (*ConfigSettings, error) {
// Read the config file
configPath := configSettingsPath()
configPath, err := configSettingsPath()
if err != nil {
return nil, err
}
contents, err := os.ReadFile(configPath)

if os.IsNotExist(err) {
Expand Down
41 changes: 14 additions & 27 deletions notehub/help-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ credentials and OAuth renewal when available.

Your workflow is: lead the user through authentication, fetch the deployed
OpenAPI specification, identify the relevant operations and resources, make
requests, and report the results. The default API base is
`https://api.notefile.net`. Use the selected hub consistently for authentication,
discovery, and requests; select another hub only when the user's task calls for it.
requests through the CLI, and report the results.

## 2. Lead the user through authentication

Expand All @@ -29,7 +27,7 @@ notehub auth status --json
```

Read the single result object's `signed_in` boolean; exit code 0 alone does not
mean signed in. If true, check the reported `hub` and `user`, then continue with
mean signed in. If true, check the reported `user`, then continue with
the task. Otherwise inspect `reason` and `action`. A network or temporary service
failure calls for retrying the check, not a new login. When sign-in is needed,
the suggested `action` is `notehub login`; as an agent, use the `--json` workflow
Expand All @@ -38,10 +36,12 @@ below.
### Preferred: guide the user through sign-in

Tell the user that you will provide a link for them to approve Notehub access.
Use a recognizable name for your agent:
Use the name of the agent harness you are running in, such as `Grok Bot`, `Muse`,
`Claude` or `Codex`, so the user can recognize which agent is requesting access.
Replace `YOUR_HARNESS_NAME` with that actual name:

```sh
notehub login --name "My Agent" --json
notehub login --name "YOUR_HARNESS_NAME" --json
```

This command opens no browser and requires no terminal input. The user opens
Expand All @@ -53,7 +53,7 @@ the approval link in their own browser, which can be on another machine.
2. **When a message contains `url`, immediately give that exact link to the
user.** In your own friendly words, ask them to open it in their browser,
sign in, and approve access for your named agent. For example:
“Please open this link in your browser to complete sign-in for My Agent.
“Please open this link in your browser to complete sign-in.
I'll keep watching here and continue once it's approved.”
The user does not need to copy anything back to you.
3. **Keep monitoring the same process's stdout.** Messages with
Expand Down Expand Up @@ -106,24 +106,12 @@ an `Authorization` header yourself.

## 3. Discover the entire public API

Fetch the deployed OpenAPI YAML from
[https://api.notefile.net/openapi.yaml](https://api.notefile.net/openapi.yaml):

```sh
curl -fsSL 'https://api.notefile.net/openapi.yaml' -o notehub-openapi.yaml
```

The CLI can fetch the selected hub's specification too:
Fetch the deployed OpenAPI YAML through the CLI:

```sh
notehub api --spec > notehub-openapi.yaml
```

Both fetches are public and need no login. For the default hub they retrieve
the same specification. If the task targets another hub, fetch that hub's spec
with `--spec --hub HUB` and use the same `--hub HUB` for subsequent commands.
The spec must describe the service you are actually calling.

**Use the live YAML as the complete catalog of public API capabilities.**
The CLI's named modes are convenient entry points; `notehub api` can call the
other operations described in the spec too, without a dedicated CLI command.
Expand Down Expand Up @@ -155,14 +143,13 @@ Read the full operation, using a YAML parser when helpful:
and any permission or resource annotations. Keep using the CLI's login
rather than implementing a separate credential exchange from the spec.

Cache the YAML for the task and refresh it when changing hubs or when an
operation appears to have changed. Discover paths and schemas from this file
Cache the YAML for the task. Refresh it when a response contradicts the contract.
Discover paths and schemas from this file
instead of inventing endpoint names or relying on deprecated APIs.

## 4. Use the CLI as an authenticated request proxy

For example, an authenticated GET to
`https://api.notefile.net/v1/projects` becomes:
For example, list the user's projects:

```sh
notehub api GET /v1/projects
Expand All @@ -188,8 +175,8 @@ Quote paths containing braces or query strings.
| Response status and headers | `--include` |
| Save the response body | Shell redirection, such as `> response.json` |

The CLI supplies authentication for each request using the selected hub's saved
login or `NOTEHUB_TOKEN`. You do not need to extract a token, add an authorization
The CLI supplies authentication for each request using the saved login or
`NOTEHUB_TOKEN`. You do not need to extract a token, add an authorization
header, or invoke curl for authenticated Notehub operations.

### Identify resources and fill paths
Expand Down Expand Up @@ -350,4 +337,4 @@ For guidance on how the user might want to interact with their Notehub work, run
notehub --agent-chat
```

It prints the guide from `https://chat.notehub.io`; no login is needed.
It prints Notehub's conversation guide; no login is needed.
Loading