Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ RUN ln -sf sciclaw /usr/local/bin/picoclaw
COPY --from=builder /src/skills /opt/sciclaw/skills

# Create default workspace with baseline skills
RUN mkdir -p /root/sciclaw/skills /root/.picoclaw && \
RUN mkdir -p /root/sciclaw/skills && \
cp -r /opt/sciclaw/skills/* /root/sciclaw/skills/ 2>/dev/null || true

ENTRYPOINT ["sciclaw"]
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1

# Workspace and Skills
PICOCLAW_HOME?=$(HOME)/.picoclaw
WORKSPACE_DIR?=$(PICOCLAW_HOME)/workspace
SCICLAW_HOME?=$(HOME)/sciclaw
PICOCLAW_HOME?=$(SCICLAW_HOME)
WORKSPACE_DIR?=$(SCICLAW_HOME)
WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills
BUILTIN_SKILLS_DIR=$(CURDIR)/skills
BUILTIN_WORKSPACE_TEMPLATES_DIR=$(CURDIR)/pkg/workspacetpl/templates/workspace
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ sciClaw's default posture is **local, private, and locked down**. Here's what th
- **Nothing is exposed to the internet.** sciClaw doesn't open any ports or listen for incoming connections. It reaches out only when you send a message, and only to the AI provider you chose (OpenAI, Anthropic, etc.) and any tools you explicitly enable (like PubMed).
- **Messages go through your private bot.** When you chat via Telegram or Discord, messages travel through a bot that only you control. Nobody else can talk to it unless you explicitly allow them.
- **No telemetry, no analytics, no tracking.** sciClaw sends nothing back to us. No usage data, no error reports, no phone-home behavior. We don't know you're running it.
- **API keys stay local.** Your credentials are stored in a config file on your machine (`~/.picoclaw/config.json`). They're never transmitted to anyone except the provider they belong to.
- **API keys stay local.** Your credentials are stored in a config file on your machine (`~/sciclaw/config.json`). They're never transmitted to anyone except the provider they belong to.
- **Skills are validated before install.** Every skill goes through size limits, binary rejection, frontmatter validation, and SHA-256 provenance logging. Catalog fetches use pinned commit refs for supply-chain hardening.

For the full security model, see [Security](https://sciclaw.dev/security.html).
Expand Down Expand Up @@ -218,7 +218,7 @@ Discord-specific features on current `main`/dev builds:
2. Run `sciclaw channels setup telegram` (pairs your account and writes config)
3. Start the gateway: `sciclaw service install && sciclaw service start`

Manual config (advanced) in `~/.picoclaw/config.json`:
Manual config (advanced) in `~/sciclaw/config.json`:

```json
{
Expand Down
5 changes: 3 additions & 2 deletions cmd/picoclaw/backup_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/paths"
)

type backupOptions struct {
Expand Down Expand Up @@ -170,7 +171,7 @@ func parseBackupOptions(args []string) (backupOptions, bool, error) {

func defaultBackupPath(homeDir string) string {
timestamp := time.Now().UTC().Format("20060102-150405")
return filepath.Join(homeDir, ".picoclaw", "backups", fmt.Sprintf("sciclaw-backup-%s.tar.gz", timestamp))
return filepath.Join(paths.BackupsDir(), fmt.Sprintf("sciclaw-backup-%s.tar.gz", timestamp))
}

func expandHomePath(path string, homeDir string) string {
Expand All @@ -184,7 +185,7 @@ func expandHomePath(path string, homeDir string) string {
}

func collectBackupEntries(cfg *config.Config, homeDir string, withSessions bool) []backupEntry {
baseDir := filepath.Join(homeDir, ".picoclaw")
baseDir := paths.AppHome()
workspace := cfg.WorkspacePath()

candidates := []backupEntry{
Expand Down
37 changes: 34 additions & 3 deletions cmd/picoclaw/doctor_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/tui"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/paths"
svcmgr "github.com/sipeed/picoclaw/pkg/service"
"github.com/sipeed/picoclaw/pkg/tools"
)
Expand Down Expand Up @@ -167,6 +168,8 @@ func runDoctor(opts doctorOptions) doctorReport {
}
}

add(checkDirUnification())

if cfgErr == nil && cfg != nil {
for _, c := range checkConfigHealth(cfg, configPath, opts) {
add(c)
Expand Down Expand Up @@ -745,12 +748,40 @@ func checkHostVMChannelConflict(hostCfg *config.Config) doctorCheck {
}
}

func checkGatewayLog(telegramEnabled bool) doctorCheck {
func checkDirUnification() doctorCheck {
home, err := os.UserHomeDir()
if err != nil {
return doctorCheck{Name: "gateway.log", Status: doctorSkip, Message: "home directory unavailable"}
return doctorCheck{Name: "home.unification", Status: doctorSkip, Message: "home directory unavailable"}
}
oldDir := filepath.Join(home, ".picoclaw")
newDir := filepath.Join(home, "sciclaw")

oldInfo, oldErr := os.Lstat(oldDir)
newInfo, newErr := os.Lstat(newDir)
if os.IsNotExist(oldErr) && os.IsNotExist(newErr) {
return doctorCheck{Name: "home.unification", Status: doctorSkip, Message: "no legacy or unified directories found"}
}
if oldErr == nil && (oldInfo.Mode()&os.ModeSymlink) != 0 {
return doctorCheck{Name: "home.unification", Status: doctorOK, Message: "~/.picoclaw is symlinked to unified root"}
}
if oldErr == nil && newErr == nil {
oldCfg := filepath.Join(oldDir, "config.json")
newCfg := filepath.Join(newDir, "config.json")
if fileExists(oldCfg) && fileExists(newCfg) {
return doctorCheck{Name: "home.unification", Status: doctorWarn, Message: "Config found in both directories — run: sciclaw migrate --unify"}
}
if oldInfo.IsDir() && newInfo.IsDir() {
return doctorCheck{Name: "home.unification", Status: doctorWarn, Message: "split home directories detected — run: sciclaw migrate --unify"}
}
}
if oldErr == nil {
return doctorCheck{Name: "home.unification", Status: doctorWarn, Message: "legacy ~/.picoclaw directory detected — run: sciclaw migrate --unify"}
}
p := filepath.Join(home, ".picoclaw", "gateway.log")
return doctorCheck{Name: "home.unification", Status: doctorOK, Message: "unified ~/sciclaw layout detected"}
}

func checkGatewayLog(telegramEnabled bool) doctorCheck {
p := filepath.Join(paths.AppHome(), "gateway.log")
if !fileExists(p) {
return doctorCheck{Name: "gateway.log", Status: doctorSkip, Message: "not found"}
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/picoclaw/env_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/sipeed/picoclaw/cmd/picoclaw/tui"
"github.com/sipeed/picoclaw/pkg/paths"
)

const defaultGHCRImage = "ghcr.io/drpedapati/sciclaw"
Expand Down Expand Up @@ -92,7 +93,7 @@ func dockerCmd() {
}
home, _ := os.UserHomeDir()
workspace := filepath.Join(home, "sciclaw")
configHome := filepath.Join(home, ".picoclaw")
configHome := paths.AppHome()
_ = os.MkdirAll(workspace, 0755)
_ = os.MkdirAll(configHome, 0755)

Expand Down
12 changes: 8 additions & 4 deletions cmd/picoclaw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/migrate"
"github.com/sipeed/picoclaw/pkg/models"
"github.com/sipeed/picoclaw/pkg/paths"
"github.com/sipeed/picoclaw/pkg/phi"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/routing"
Expand Down Expand Up @@ -1181,6 +1182,8 @@ func migrateCmd() {
opts.WorkspaceOnly = true
case "--force":
opts.Force = true
case "--unify":
opts.Unify = true
case "--refresh":
opts.Refresh = true
case "--openclaw-home":
Expand Down Expand Up @@ -1223,6 +1226,7 @@ func migrateHelp() {
fmt.Println(" --config-only Only migrate config, skip workspace files")
fmt.Println(" --workspace-only Only migrate workspace files, skip config")
fmt.Println(" --force Skip confirmation prompts")
fmt.Println(" --unify Unify ~/.picoclaw into ~/sciclaw and create symlink")
fmt.Println(" --openclaw-home Override OpenClaw home directory (default: ~/.openclaw)")
fmt.Println(" --picoclaw-home Override PicoClaw home directory (default: ~/.picoclaw)")
fmt.Println()
Expand All @@ -1231,6 +1235,7 @@ func migrateHelp() {
fmt.Printf(" %s migrate --dry-run Show what would be migrated\n", commandName)
fmt.Printf(" %s migrate --refresh Re-sync workspace files\n", commandName)
fmt.Printf(" %s migrate --force Migrate without confirmation\n", commandName)
fmt.Printf(" %s migrate --unify Unify ~/.picoclaw into ~/sciclaw\n", commandName)
}

func agentCmd() {
Expand Down Expand Up @@ -1452,8 +1457,8 @@ func gatewayCmd() {

// Resolve home directory once for all path construction below (status
// file, log file, etc.). If it fails, skip home-dependent setup.
gwHome, gwHomeErr := os.UserHomeDir()
picoDir := filepath.Join(gwHome, ".picoclaw")
_, gwHomeErr := os.UserHomeDir()
picoDir := paths.AppHome()

// Kill any stale gateway process from a previous run (e.g. orphaned SSH session).
// The status file is written on startup and removed on clean shutdown — if it
Expand Down Expand Up @@ -2676,8 +2681,7 @@ func authStatusCmd() {
}

func getConfigPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "config.json")
return paths.ConfigPath()
}

func runGatewayDefaultDispatch(ctx context.Context, msgBus *bus.MessageBus, agentLoop *agent.AgentLoop, jobManager *routing.JobManager, target routing.LoopTarget) {
Expand Down
6 changes: 4 additions & 2 deletions cmd/picoclaw/tui/executor_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"time"

"github.com/sipeed/picoclaw/pkg/paths"
)

// LocalExecutor runs commands directly on the host.
Expand Down Expand Up @@ -86,11 +88,11 @@ func (e *LocalExecutor) WriteFile(path string, data []byte, perm os.FileMode) er
}

func (e *LocalExecutor) ConfigPath() string {
return filepath.Join(e.home, ".picoclaw", "config.json")
return paths.ConfigPath()
}

func (e *LocalExecutor) AuthPath() string {
return filepath.Join(e.home, ".picoclaw", "auth.json")
return paths.AuthPath()
}

func (e *LocalExecutor) HomePath() string { return e.home }
Expand Down
8 changes: 3 additions & 5 deletions cmd/picoclaw/tui/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"
"sync"
"time"

"github.com/sipeed/picoclaw/pkg/paths"
)

// VMSnapshot holds the complete runtime state collected from the VM or local host.
Expand Down Expand Up @@ -425,11 +427,7 @@ func shellEscape(s string) string {
}

func hostConfigRaw() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
path := filepath.Join(home, ".picoclaw", "config.json")
path := paths.ConfigPath()
b, err := os.ReadFile(path)
if err != nil {
return "", err
Expand Down
4 changes: 2 additions & 2 deletions cmd/picoclaw/tui/tab_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,12 +549,12 @@ func fetchSettingsData(exec Executor) tea.Cmd {

// Config bootstrap: create defaults only when config is missing.
home := exec.HomePath()
_, _ = exec.ExecShell(5*time.Second, "mkdir -p "+shellEscape(home+"/.picoclaw/workspace"))
_, _ = exec.ExecShell(5*time.Second, "mkdir -p "+shellEscape(home+"/sciclaw"))
cfg = map[string]interface{}{
"agents": map[string]interface{}{
"defaults": map[string]interface{}{
"model": "gpt-5.2",
"workspace": "~/.picoclaw/workspace",
"workspace": "~/sciclaw",
},
},
"channels": map[string]interface{}{
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
profiles:
- agent
volumes:
- ./config/config.json:/root/.picoclaw/config.json:ro
- ./config/config.json:/root/sciclaw/config.json:ro
- sciclaw-workspace:/root/sciclaw
entrypoint: ["sciclaw", "agent"]
stdin_open: true
Expand All @@ -31,7 +31,7 @@ services:
- gateway
volumes:
# Configuration file
- ./config/config.json:/root/.picoclaw/config.json:ro
- ./config/config.json:/root/sciclaw/config.json:ro
# Persistent workspace (sessions, memory, logs)
- sciclaw-workspace:/root/sciclaw
command: ["gateway"]
Expand Down
18 changes: 9 additions & 9 deletions docs/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ <h3>Step 2 &mdash; Initialize (app or onboard)</h3>

<span class="hl-comment"># CLI alternative</span>
<span class="hl-cmd">sciclaw</span> onboard</code></pre>
<p>Both commands run the same first-time setup flow. It creates your config (<code>~/.picoclaw/config.json</code>), workspace directories (<code>~/sciclaw/</code> with <code>memory/</code>, <code>skills/</code>, <code>sessions/</code>, <code>cron/</code>), starter template files, and the baseline science skill pack. On Linux it also bootstraps a managed workspace Python venv at <code>~/sciclaw/.venv</code> (prefers <code>uv</code>, falls back to <code>python3 -m venv</code>) and installs required packages used by scientific workflows. Then it asks optional questions:</p>
<p>Both commands run the same first-time setup flow. It creates your config (<code>~/sciclaw/config.json</code>), workspace directories (<code>~/sciclaw/</code> with <code>memory/</code>, <code>skills/</code>, <code>sessions/</code>, <code>cron/</code>), starter template files, and the baseline science skill pack. On Linux it also bootstraps a managed workspace Python venv at <code>~/sciclaw/.venv</code> (prefers <code>uv</code>, falls back to <code>python3 -m venv</code>) and installs required packages used by scientific workflows. Then it asks optional questions:</p>
<ol>
<li><strong>OpenAI login</strong> &mdash; Uses device-code login for ChatGPT Plus, Team, or Enterprise (a link opens in your browser). Skip this if you plan to use a different provider.</li>
<li><strong>Chat smoke test</strong> &mdash; If authentication succeeds, a single test message is sent automatically to confirm your AI provider is connected and responding.</li>
Expand Down Expand Up @@ -861,7 +861,7 @@ <h3>Verifying the update</h3>
<tr><td><code>irl</code> runtime</td><td><code>brew upgrade sciclaw</code> (dependency)</td><td><code>$(brew --prefix)/bin/irl</code></td></tr>
<tr><td><code>quarto</code> (optional)</td><td>macOS: <code>brew upgrade --cask quarto</code> / Linux: <code>brew upgrade sciclaw</code> (dependency)</td><td><code>$(brew --prefix)/bin/quarto</code></td></tr>
<tr><td>Baseline skills</td><td><code>sciclaw skills install-builtin</code></td><td><code>~/sciclaw/skills/</code></td></tr>
<tr><td>Config file</td><td>Manual or <code>sciclaw models set</code></td><td><code>~/.picoclaw/config.json</code></td></tr>
<tr><td>Config file</td><td>Manual or <code>sciclaw models set</code></td><td><code>~/sciclaw/config.json</code></td></tr>
<tr><td>Workspace templates</td><td><code>sciclaw onboard</code></td><td><code>~/sciclaw/</code></td></tr>
</tbody>
</table>
Expand Down Expand Up @@ -1172,7 +1172,7 @@ <h3>Recommended setup (Telegram)</h3>
<div class="callout">
<div class="callout-title">Where your work lives</div>
<p><strong>Workspace (visible):</strong> <code>~/sciclaw/</code> (sessions, evidence, skills, hooks, IRL audit logs).</p>
<p><strong>Config + auth (hidden):</strong> <code>~/.picoclaw/config.json</code> and <code>~/.picoclaw/auth.json</code> (secrets and provider credentials).</p>
<p><strong>Config + auth (hidden):</strong> <code>~/sciclaw/config.json</code> and <code>~/sciclaw/auth.json</code> (secrets and provider credentials).</p>
</div>

<div class="callout callout-warn">
Expand All @@ -1198,7 +1198,7 @@ <h3>Keep it up to date</h3>
<!-- ━━━━━ CONFIGURATION ━━━━━ -->
<div class="section" id="configuration">
<h2>Configuration</h2>
<p class="page-subtitle">All settings live in <code>~/.picoclaw/config.json</code>.</p>
<p class="page-subtitle">All settings live in <code>~/sciclaw/config.json</code>.</p>

<p>The config file controls agent defaults, provider credentials, channel integrations, tool settings, and heartbeat behavior. A complete example is shipped at <code>config/config.example.json</code> in the repository.</p>

Expand Down Expand Up @@ -1326,7 +1326,7 @@ <h4>Auth commands</h4>
<span class="hl-comment"># Remove stored credential</span>
<span class="hl-cmd">sciclaw</span> auth logout <span class="hl-flag">--provider</span> openai</code></pre>

<p>Credentials are stored in <code>~/.picoclaw/auth.json</code> with user-only file permissions. A successful login sets <code>providers.&lt;name&gt;.auth_method</code> in the config (<code>oauth</code> or <code>token</code>). On current Anthropic dev builds, oat-token credentials can be routed through the companion Claude bridge automatically when available.</p>
<p>Credentials are stored in <code>~/sciclaw/auth.json</code> with user-only file permissions. A successful login sets <code>providers.&lt;name&gt;.auth_method</code> in the config (<code>oauth</code> or <code>token</code>). On current Anthropic dev builds, oat-token credentials can be routed through the companion Claude bridge automatically when available.</p>
</div>

<!-- ─── Workspace ─── -->
Expand All @@ -1351,7 +1351,7 @@ <h3>Workspace Layout</h3>
├── TOOLS.md <span class="hl-comment"># Tool descriptions</span>
└── USER.md <span class="hl-comment"># User preferences</span></code></pre>

<p>Bootstrap templates are versioned at <code>pkg/workspacetpl/templates/workspace/</code> and also installed to <code>~/.picoclaw/templates/workspace/</code> for review and customization.</p>
<p>Bootstrap templates are versioned at <code>pkg/workspacetpl/templates/workspace/</code> and also installed to <code>~/sciclaw/templates/workspace/</code> for review and customization.</p>
</div>

<hr />
Expand Down Expand Up @@ -1422,7 +1422,7 @@ <h3>Agent</h3>
<!-- ─── Model Switching ─── -->
<div class="section" id="model-switching">
<h3>Model Switching</h3>
<p>Previously, switching models required editing <code>~/.picoclaw/config.json</code> by hand and restarting. sciClaw now supports three ways to change models &mdash; a one-shot CLI flag, a persistent subcommand, and the config file. In practice, use <code>gpt-5.2</code> as the primary path and tune reasoning effort per task.</p>
<p>Previously, switching models required editing <code>~/sciclaw/config.json</code> by hand and restarting. sciClaw now supports three ways to change models &mdash; a one-shot CLI flag, a persistent subcommand, and the config file. In practice, use <code>gpt-5.2</code> as the primary path and tune reasoning effort per task.</p>

<h4>1. Per-invocation override (<code>--model</code>)</h4>
<p>The <code>--model</code> flag overrides the model for a single command. Your config file is <strong>not changed</strong>, so the next invocation returns to your default. Use this when testing a model, when your primary provider is down, or when a specific task needs a different model.</p>
Expand Down Expand Up @@ -1450,7 +1450,7 @@ <h4>2. Persistent change (<code>sciclaw models</code>)</h4>
<span class="hl-comment"># Provider: openai</span></code></pre>

<h4>3. Config file</h4>
<p>You can also set the model directly in <code>~/.picoclaw/config.json</code>. This is the same file that <code>sciclaw models set</code> writes to.</p>
<p>You can also set the model directly in <code>~/sciclaw/config.json</code>. This is the same file that <code>sciclaw models set</code> writes to.</p>
<pre><code>{
<span class="hl-key">"agents"</span>: {
<span class="hl-key">"defaults"</span>: {
Expand Down Expand Up @@ -1599,7 +1599,7 @@ <h4>What it checks</h4>
<tr><th>Check</th><th>What it verifies</th></tr>
</thead>
<tbody>
<tr><td><code>config</code></td><td><code>~/.picoclaw/config.json</code> exists and loads</td></tr>
<tr><td><code>config</code></td><td><code>~/sciclaw/config.json</code> exists and loads</td></tr>
<tr><td><code>workspace</code></td><td>Workspace directory exists</td></tr>
<tr><td><code>auth.openai</code></td><td>OAuth status: authenticated, expired, or needs refresh</td></tr>
<tr><td><code>auth.anthropic</code></td><td>Anthropic auth status, including Claude OAuth/oat-token bridge state when configured</td></tr>
Expand Down
Loading
Loading