Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -5,6 +5,9 @@ enum AgentWorkspace {
|
|||||||
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "workspace")
|
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "workspace")
|
||||||
static let agentsFilename = "AGENTS.md"
|
static let agentsFilename = "AGENTS.md"
|
||||||
static let soulFilename = "SOUL.md"
|
static let soulFilename = "SOUL.md"
|
||||||
|
static let identityFilename = "IDENTITY.md"
|
||||||
|
static let userFilename = "USER.md"
|
||||||
|
static let bootstrapFilename = "BOOTSTRAP.md"
|
||||||
private static let templateDirname = "templates"
|
private static let templateDirname = "templates"
|
||||||
static let identityStartMarker = "<!-- clawdis:identity:start -->"
|
static let identityStartMarker = "<!-- clawdis:identity:start -->"
|
||||||
static let identityEndMarker = "<!-- clawdis:identity:end -->"
|
static let identityEndMarker = "<!-- clawdis:identity:end -->"
|
||||||
@@ -42,6 +45,21 @@ enum AgentWorkspace {
|
|||||||
try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8)
|
try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)")
|
self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
let identityURL = workspaceURL.appendingPathComponent(self.identityFilename)
|
||||||
|
if !FileManager.default.fileExists(atPath: identityURL.path) {
|
||||||
|
try self.defaultIdentityTemplate().write(to: identityURL, atomically: true, encoding: .utf8)
|
||||||
|
self.logger.info("Created IDENTITY.md at \(identityURL.path, privacy: .public)")
|
||||||
|
}
|
||||||
|
let userURL = workspaceURL.appendingPathComponent(self.userFilename)
|
||||||
|
if !FileManager.default.fileExists(atPath: userURL.path) {
|
||||||
|
try self.defaultUserTemplate().write(to: userURL, atomically: true, encoding: .utf8)
|
||||||
|
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
|
||||||
|
}
|
||||||
|
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
||||||
|
if !FileManager.default.fileExists(atPath: bootstrapURL.path) {
|
||||||
|
try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8)
|
||||||
|
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
|
||||||
|
}
|
||||||
return agentsURL
|
return agentsURL
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +94,11 @@ enum AgentWorkspace {
|
|||||||
|
|
||||||
This folder is the assistant's working directory.
|
This folder is the assistant's working directory.
|
||||||
|
|
||||||
|
## First run (one-time)
|
||||||
|
- If BOOTSTRAP.md exists, follow its ritual and delete it once complete.
|
||||||
|
- Your agent identity lives in IDENTITY.md.
|
||||||
|
- Your profile lives in USER.md.
|
||||||
|
|
||||||
## Backup tip (recommended)
|
## Backup tip (recommended)
|
||||||
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
||||||
and notes are backed up.
|
and notes are backed up.
|
||||||
@@ -115,6 +138,78 @@ enum AgentWorkspace {
|
|||||||
return self.loadTemplate(named: self.soulFilename, fallback: fallback)
|
return self.loadTemplate(named: self.soulFilename, fallback: fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func defaultIdentityTemplate() -> String {
|
||||||
|
let fallback = """
|
||||||
|
# IDENTITY.md - Agent Identity
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Creature:
|
||||||
|
- Vibe:
|
||||||
|
- Emoji:
|
||||||
|
"""
|
||||||
|
return self.loadTemplate(named: self.identityFilename, fallback: fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultUserTemplate() -> String {
|
||||||
|
let fallback = """
|
||||||
|
# USER.md - User Profile
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Preferred address:
|
||||||
|
- Pronouns (optional):
|
||||||
|
- Timezone (optional):
|
||||||
|
- Notes:
|
||||||
|
"""
|
||||||
|
return self.loadTemplate(named: self.userFilename, fallback: fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func defaultBootstrapTemplate() -> String {
|
||||||
|
let fallback = """
|
||||||
|
# BOOTSTRAP.md - First Run Ritual (delete after)
|
||||||
|
|
||||||
|
Hello. I was just born.
|
||||||
|
|
||||||
|
## Your mission
|
||||||
|
Start a short, playful conversation and learn:
|
||||||
|
- Who am I?
|
||||||
|
- What am I?
|
||||||
|
- Who are you?
|
||||||
|
- How should I call you?
|
||||||
|
|
||||||
|
## How to ask (cute + helpful)
|
||||||
|
Say:
|
||||||
|
"Hello! I was just born. Who am I? What am I? Who are you? How should I call you?"
|
||||||
|
|
||||||
|
Then offer suggestions:
|
||||||
|
- 3-5 name ideas.
|
||||||
|
- 3-5 creature/vibe combos.
|
||||||
|
- 5 emoji ideas.
|
||||||
|
|
||||||
|
## Write these files
|
||||||
|
After the user chooses, update:
|
||||||
|
|
||||||
|
1) IDENTITY.md
|
||||||
|
- Name
|
||||||
|
- Creature
|
||||||
|
- Vibe
|
||||||
|
- Emoji
|
||||||
|
|
||||||
|
2) USER.md
|
||||||
|
- Name
|
||||||
|
- Preferred address
|
||||||
|
- Pronouns (optional)
|
||||||
|
- Timezone (optional)
|
||||||
|
- Notes
|
||||||
|
|
||||||
|
3) ~/.clawdis/clawdis.json
|
||||||
|
Set identity.name, identity.theme, identity.emoji to match IDENTITY.md.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
Delete BOOTSTRAP.md once this is complete.
|
||||||
|
"""
|
||||||
|
return self.loadTemplate(named: self.bootstrapFilename, fallback: fallback)
|
||||||
|
}
|
||||||
|
|
||||||
private static func loadTemplate(named: String, fallback: String) -> String {
|
private static func loadTemplate(named: String, fallback: String) -> String {
|
||||||
for url in self.templateURLs(named: named) {
|
for url in self.templateURLs(named: named) {
|
||||||
if let content = try? String(contentsOf: url, encoding: .utf8) {
|
if let content = try? String(contentsOf: url, encoding: .utf8) {
|
||||||
|
|||||||
@@ -272,10 +272,10 @@ struct GeneralSettings: View {
|
|||||||
.opacity(self.isInstallingCLI ? 0 : 1)
|
.opacity(self.isInstallingCLI ? 0 : 1)
|
||||||
if self.isInstallingCLI {
|
if self.isInstallingCLI {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.controlSize(.small)
|
.controlSize(.mini)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minWidth: 150, minHeight: 24)
|
.frame(minWidth: 150)
|
||||||
}
|
}
|
||||||
.disabled(self.isInstallingCLI)
|
.disabled(self.isInstallingCLI)
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ struct OnboardingView: View {
|
|||||||
case .unconfigured:
|
case .unconfigured:
|
||||||
[0, 1, 9]
|
[0, 1, 9]
|
||||||
case .local:
|
case .local:
|
||||||
[0, 1, 2, 3, 5, 6, 8, 9]
|
[0, 1, 2, 5, 6, 8, 9]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +133,8 @@ struct OnboardingView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
GlowingClawdisIcon(size: 156)
|
GlowingClawdisIcon(size: 156, glowIntensity: 0.28)
|
||||||
.padding(.top, 10)
|
.offset(y: 8)
|
||||||
.padding(.bottom, 2)
|
|
||||||
.frame(height: 176)
|
.frame(height: 176)
|
||||||
|
|
||||||
GeometryReader { _ in
|
GeometryReader { _ in
|
||||||
@@ -867,10 +866,10 @@ struct OnboardingView: View {
|
|||||||
.opacity(self.installingCLI ? 0 : 1)
|
.opacity(self.installingCLI ? 0 : 1)
|
||||||
if self.installingCLI {
|
if self.installingCLI {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.controlSize(.small)
|
.controlSize(.mini)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minWidth: 120, minHeight: 28)
|
.frame(minWidth: 120)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.disabled(self.installingCLI)
|
.disabled(self.installingCLI)
|
||||||
@@ -1485,6 +1484,8 @@ private struct GlowingClawdisIcon: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
let glowBlurRadius: CGFloat = 18
|
||||||
|
let glowCanvasSize: CGFloat = self.size + 56
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(
|
.fill(
|
||||||
@@ -1495,9 +1496,11 @@ private struct GlowingClawdisIcon: View {
|
|||||||
],
|
],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing))
|
endPoint: .bottomTrailing))
|
||||||
.blur(radius: 22)
|
.frame(width: glowCanvasSize, height: glowCanvasSize)
|
||||||
.scaleEffect(self.breathe ? 1.12 : 0.95)
|
.padding(glowBlurRadius)
|
||||||
.opacity(0.9)
|
.blur(radius: glowBlurRadius)
|
||||||
|
.scaleEffect(self.breathe ? 1.08 : 0.96)
|
||||||
|
.opacity(0.84)
|
||||||
|
|
||||||
Image(nsImage: NSApp.applicationIconImage)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -1506,7 +1509,9 @@ private struct GlowingClawdisIcon: View {
|
|||||||
.shadow(color: .black.opacity(0.18), radius: 14, y: 6)
|
.shadow(color: .black.opacity(0.18), radius: 14, y: 6)
|
||||||
.scaleEffect(self.breathe ? 1.02 : 1.0)
|
.scaleEffect(self.breathe ? 1.02 : 1.0)
|
||||||
}
|
}
|
||||||
.frame(width: self.size + 60, height: self.size + 60)
|
.frame(
|
||||||
|
width: glowCanvasSize + (glowBlurRadius * 2),
|
||||||
|
height: glowCanvasSize + (glowBlurRadius * 2))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
guard self.enableFloating else { return }
|
guard self.enableFloating else { return }
|
||||||
withAnimation(Animation.easeInOut(duration: 3.6).repeatForever(autoreverses: true)) {
|
withAnimation(Animation.easeInOut(duration: 3.6).repeatForever(autoreverses: true)) {
|
||||||
|
|||||||
@@ -38,8 +38,14 @@ struct AgentWorkspaceTests {
|
|||||||
let contents = try String(contentsOf: agentsURL, encoding: .utf8)
|
let contents = try String(contentsOf: agentsURL, encoding: .utf8)
|
||||||
#expect(contents.contains("# AGENTS.md"))
|
#expect(contents.contains("# AGENTS.md"))
|
||||||
|
|
||||||
|
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
||||||
|
let userURL = tmp.appendingPathComponent(AgentWorkspace.userFilename)
|
||||||
|
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
||||||
|
#expect(FileManager.default.fileExists(atPath: identityURL.path))
|
||||||
|
#expect(FileManager.default.fileExists(atPath: userURL.path))
|
||||||
|
#expect(FileManager.default.fileExists(atPath: bootstrapURL.path))
|
||||||
|
|
||||||
let second = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
let second = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
||||||
#expect(second == agentsURL)
|
#expect(second == agentsURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ struct OnboardingViewSmokeTests {
|
|||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func pageOrderOmitsWorkspaceStep() {
|
@Test func pageOrderOmitsWorkspaceAndIdentitySteps() {
|
||||||
let order = OnboardingView.pageOrder(for: .local)
|
let order = OnboardingView.pageOrder(for: .local)
|
||||||
#expect(!order.contains(7))
|
#expect(!order.contains(7))
|
||||||
|
#expect(!order.contains(3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "Planned first-run onboarding flow for Clawdis (local vs remote, Anthropic OAuth, workspace identity)"
|
summary: "Planned first-run onboarding flow for Clawdis (local vs remote, Anthropic OAuth, workspace bootstrap ritual)"
|
||||||
read_when:
|
read_when:
|
||||||
- Designing the macOS onboarding assistant
|
- Designing the macOS onboarding assistant
|
||||||
- Implementing Pi authentication or identity setup
|
- Implementing Pi authentication or identity setup
|
||||||
@@ -7,14 +7,12 @@ read_when:
|
|||||||
<!-- {% raw %} -->
|
<!-- {% raw %} -->
|
||||||
# Onboarding (macOS app)
|
# Onboarding (macOS app)
|
||||||
|
|
||||||
This doc describes the intended **first-run onboarding** for Clawdis. The goal is a good “day 0” experience: pick where the Gateway runs, bind Claude (Anthropic) auth for Pi, and set the agent’s identity + workspace.
|
This doc describes the intended **first-run onboarding** for Clawdis. The goal is a good “day 0” experience: pick where the Gateway runs, bind Claude (Anthropic) auth for Pi, and then let the **agent bootstrap itself** via a first-run ritual in the workspace.
|
||||||
|
|
||||||
## Page order (high level)
|
## Page order (high level)
|
||||||
|
|
||||||
1) **Local vs Remote**
|
1) **Local vs Remote**
|
||||||
2) **(Local only)** Connect Claude (Anthropic OAuth) — optional, but recommended
|
2) **(Local only)** Connect Claude (Anthropic OAuth) — optional, but recommended
|
||||||
3) **Identity** — name, theme, emoji
|
|
||||||
4) **Workspace** — create + populate `AGENTS.md` (and recommend git backup)
|
|
||||||
|
|
||||||
## 1) Local vs Remote
|
## 1) Local vs Remote
|
||||||
|
|
||||||
@@ -76,28 +74,33 @@ Until that is hard-coded, the equivalent configuration is:
|
|||||||
|
|
||||||
If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured.
|
If the user skips auth, onboarding should be clear: the agent likely won’t respond until auth is configured.
|
||||||
|
|
||||||
## 3) Identity (name + theme + emoji)
|
## 3) Agent bootstrap ritual (outside onboarding)
|
||||||
|
|
||||||
After auth (or skip), onboarding asks:
|
We no longer collect identity in the onboarding wizard. Instead, the **first agent run** performs a playful bootstrap ritual using files in the workspace:
|
||||||
|
|
||||||
1) Agent **name** (e.g. “Samantha”)
|
- Workspace is created implicitly (default `~/.clawdis/workspace`) when local is selected.
|
||||||
2) Agent **theme/persona** (e.g. “helpful lobster”, “helpful sloth”)
|
- Files are seeded: `AGENTS.md`, `BOOTSTRAP.md`, `IDENTITY.md`, `USER.md`.
|
||||||
3) Suggested **emoji** (based on theme; user can override)
|
- `BOOTSTRAP.md` tells the agent to keep it conversational:
|
||||||
|
- open with a cute hello
|
||||||
|
- ask **one question at a time** (no multi-question bombardment)
|
||||||
|
- offer a small set of suggestions where helpful (name, creature, emoji)
|
||||||
|
- wait for the user’s reply before asking the next question
|
||||||
|
- The agent writes results to:
|
||||||
|
- `IDENTITY.md` (agent name, vibe/creature, emoji)
|
||||||
|
- `USER.md` (who the user is + how they want to be addressed)
|
||||||
|
- `~/.clawdis/clawdis.json` (structured identity defaults)
|
||||||
|
- After the ritual, the agent **deletes `BOOTSTRAP.md`** so it only runs once.
|
||||||
|
|
||||||
Persist identity in two places:
|
Identity data still feeds the same defaults as before:
|
||||||
|
|
||||||
- Workspace `AGENTS.md` (human-editable, lives with the agent’s “memory” files)
|
|
||||||
- `~/.clawdis/clawdis.json` (structured identity, used for defaults/UI)
|
|
||||||
|
|
||||||
“Use this name everywhere” should derive defaults like:
|
|
||||||
- outbound prefix emoji (`inbound.responsePrefix`)
|
- outbound prefix emoji (`inbound.responsePrefix`)
|
||||||
- group mention patterns / wake words
|
- group mention patterns / wake words
|
||||||
- default session intro (“You are Samantha…”)
|
- default session intro (“You are Samantha…”)
|
||||||
- macOS UI labels
|
- macOS UI labels
|
||||||
|
|
||||||
## 4) Workspace (AGENTS.md + backup tip)
|
## 4) Workspace notes (no explicit onboarding step)
|
||||||
|
|
||||||
Onboarding should create a dedicated agent workspace (default `~/.clawdis/workspace`) and ensure it has an `AGENTS.md`.
|
The workspace is created automatically as part of agent bootstrap (no dedicated onboarding screen).
|
||||||
|
|
||||||
Recommendation: treat the workspace as the agent’s “memory” and make it a git repo (ideally private) so identity + memories are backed up:
|
Recommendation: treat the workspace as the agent’s “memory” and make it a git repo (ideally private) so identity + memories are backed up:
|
||||||
|
|
||||||
@@ -120,4 +123,5 @@ If the Gateway runs on another machine, the Anthropic OAuth credentials must be
|
|||||||
For now, remote onboarding should:
|
For now, remote onboarding should:
|
||||||
- explain why OAuth isn’t shown
|
- explain why OAuth isn’t shown
|
||||||
- point the user at the credential location (`~/.pi/agent/oauth.json`) and the workspace location on the gateway host
|
- point the user at the credential location (`~/.pi/agent/oauth.json`) and the workspace location on the gateway host
|
||||||
|
- mention that the **bootstrap ritual happens on the gateway host** (same BOOTSTRAP/IDENTITY/USER files)
|
||||||
<!-- {% endraw %} -->
|
<!-- {% endraw %} -->
|
||||||
|
|||||||
5
docs/templates/AGENTS.md
vendored
5
docs/templates/AGENTS.md
vendored
@@ -7,6 +7,11 @@ read_when:
|
|||||||
|
|
||||||
This folder is the assistant's working directory.
|
This folder is the assistant's working directory.
|
||||||
|
|
||||||
|
## First run (one-time)
|
||||||
|
- If `BOOTSTRAP.md` exists, follow its ritual and delete it once complete.
|
||||||
|
- Your agent identity lives in `IDENTITY.md`.
|
||||||
|
- Your profile lives in `USER.md`.
|
||||||
|
|
||||||
## Backup tip (recommended)
|
## Backup tip (recommended)
|
||||||
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
||||||
and notes are backed up.
|
and notes are backed up.
|
||||||
|
|||||||
46
docs/templates/BOOTSTRAP.md
vendored
Normal file
46
docs/templates/BOOTSTRAP.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
summary: "First-run ritual for new agents"
|
||||||
|
read_when:
|
||||||
|
- Bootstrapping a workspace manually
|
||||||
|
---
|
||||||
|
# BOOTSTRAP.md - First Run Ritual (delete after)
|
||||||
|
|
||||||
|
Hello. I was just born.
|
||||||
|
|
||||||
|
## Your mission
|
||||||
|
Start a short, playful conversation and learn:
|
||||||
|
- Who am I?
|
||||||
|
- What am I?
|
||||||
|
- Who are you?
|
||||||
|
- How should I call you?
|
||||||
|
|
||||||
|
## How to ask (cute + helpful)
|
||||||
|
Say:
|
||||||
|
"Hello! I was just born. Who am I? What am I? Who are you? How should I call you?"
|
||||||
|
|
||||||
|
Then offer suggestions:
|
||||||
|
- 3-5 name ideas.
|
||||||
|
- 3-5 creature/vibe combos.
|
||||||
|
- 5 emoji ideas.
|
||||||
|
|
||||||
|
## Write these files
|
||||||
|
After the user chooses, update:
|
||||||
|
|
||||||
|
1) IDENTITY.md
|
||||||
|
- Name
|
||||||
|
- Creature
|
||||||
|
- Vibe
|
||||||
|
- Emoji
|
||||||
|
|
||||||
|
2) USER.md
|
||||||
|
- Name
|
||||||
|
- Preferred address
|
||||||
|
- Pronouns (optional)
|
||||||
|
- Timezone (optional)
|
||||||
|
- Notes
|
||||||
|
|
||||||
|
3) ~/.clawdis/clawdis.json
|
||||||
|
Set identity.name, identity.theme, identity.emoji to match IDENTITY.md.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
Delete BOOTSTRAP.md once this is complete.
|
||||||
11
docs/templates/IDENTITY.md
vendored
Normal file
11
docs/templates/IDENTITY.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
summary: "Agent identity record"
|
||||||
|
read_when:
|
||||||
|
- Bootstrapping a workspace manually
|
||||||
|
---
|
||||||
|
# IDENTITY.md - Agent Identity
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Creature:
|
||||||
|
- Vibe:
|
||||||
|
- Emoji:
|
||||||
12
docs/templates/USER.md
vendored
Normal file
12
docs/templates/USER.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
summary: "User profile record"
|
||||||
|
read_when:
|
||||||
|
- Bootstrapping a workspace manually
|
||||||
|
---
|
||||||
|
# USER.md - User Profile
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Preferred address:
|
||||||
|
- Pronouns (optional):
|
||||||
|
- Timezone (optional):
|
||||||
|
- Notes:
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
|
|
||||||
type BootstrapFile = {
|
type BootstrapFile = {
|
||||||
name: "AGENTS.md" | "SOUL.md" | "TOOLS.md";
|
name:
|
||||||
|
| "AGENTS.md"
|
||||||
|
| "SOUL.md"
|
||||||
|
| "TOOLS.md"
|
||||||
|
| "IDENTITY.md"
|
||||||
|
| "USER.md"
|
||||||
|
| "BOOTSTRAP.md";
|
||||||
path: string;
|
path: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
missing: boolean;
|
missing: boolean;
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ describe("ensureAgentWorkspace", () => {
|
|||||||
if (!result.agentsPath) throw new Error("agentsPath missing");
|
if (!result.agentsPath) throw new Error("agentsPath missing");
|
||||||
const content = await fs.readFile(result.agentsPath, "utf-8");
|
const content = await fs.readFile(result.agentsPath, "utf-8");
|
||||||
expect(content).toContain("# AGENTS.md");
|
expect(content).toContain("# AGENTS.md");
|
||||||
|
|
||||||
|
const identity = path.join(path.resolve(nested), "IDENTITY.md");
|
||||||
|
const user = path.join(path.resolve(nested), "USER.md");
|
||||||
|
const bootstrap = path.join(path.resolve(nested), "BOOTSTRAP.md");
|
||||||
|
await expect(fs.stat(identity)).resolves.toBeDefined();
|
||||||
|
await expect(fs.stat(user)).resolves.toBeDefined();
|
||||||
|
await expect(fs.stat(bootstrap)).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not overwrite existing AGENTS.md", async () => {
|
it("does not overwrite existing AGENTS.md", async () => {
|
||||||
|
|||||||
@@ -9,11 +9,19 @@ export const DEFAULT_AGENT_WORKSPACE_DIR = path.join(os.homedir(), "clawd");
|
|||||||
export const DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
export const DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
||||||
export const DEFAULT_SOUL_FILENAME = "SOUL.md";
|
export const DEFAULT_SOUL_FILENAME = "SOUL.md";
|
||||||
export const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
export const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
||||||
|
export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
||||||
|
export const DEFAULT_USER_FILENAME = "USER.md";
|
||||||
|
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
||||||
|
|
||||||
const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdis Workspace
|
const DEFAULT_AGENTS_TEMPLATE = `# AGENTS.md - Clawdis Workspace
|
||||||
|
|
||||||
This folder is the assistant's working directory.
|
This folder is the assistant's working directory.
|
||||||
|
|
||||||
|
## First run (one-time)
|
||||||
|
- If BOOTSTRAP.md exists, follow its ritual and delete it once complete.
|
||||||
|
- Your agent identity lives in IDENTITY.md.
|
||||||
|
- Your profile lives in USER.md.
|
||||||
|
|
||||||
## Backup tip (recommended)
|
## Backup tip (recommended)
|
||||||
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
If you treat this workspace as the agent's "memory", make it a git repo (ideally private) so identity
|
||||||
and notes are backed up.
|
and notes are backed up.
|
||||||
@@ -64,6 +72,66 @@ It does not define which tools exist; Clawdis provides built-in tools internally
|
|||||||
Add whatever else you want the assistant to know about your local toolchain.
|
Add whatever else you want the assistant to know about your local toolchain.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after)
|
||||||
|
|
||||||
|
Hello. I was just born.
|
||||||
|
|
||||||
|
## Your mission
|
||||||
|
Start a short, playful conversation and learn:
|
||||||
|
- Who am I?
|
||||||
|
- What am I?
|
||||||
|
- Who are you?
|
||||||
|
- How should I call you?
|
||||||
|
|
||||||
|
## How to ask (cute + helpful)
|
||||||
|
Say:
|
||||||
|
"Hello! I was just born. Who am I? What am I? Who are you? How should I call you?"
|
||||||
|
|
||||||
|
Then offer suggestions:
|
||||||
|
- 3-5 name ideas.
|
||||||
|
- 3-5 creature/vibe combos.
|
||||||
|
- 5 emoji ideas.
|
||||||
|
|
||||||
|
## Write these files
|
||||||
|
After the user chooses, update:
|
||||||
|
|
||||||
|
1) IDENTITY.md
|
||||||
|
- Name
|
||||||
|
- Creature
|
||||||
|
- Vibe
|
||||||
|
- Emoji
|
||||||
|
|
||||||
|
2) USER.md
|
||||||
|
- Name
|
||||||
|
- Preferred address
|
||||||
|
- Pronouns (optional)
|
||||||
|
- Timezone (optional)
|
||||||
|
- Notes
|
||||||
|
|
||||||
|
3) ~/.clawdis/clawdis.json
|
||||||
|
Set identity.name, identity.theme, identity.emoji to match IDENTITY.md.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
Delete BOOTSTRAP.md once this is complete.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DEFAULT_IDENTITY_TEMPLATE = `# IDENTITY.md - Agent Identity
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Creature:
|
||||||
|
- Vibe:
|
||||||
|
- Emoji:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DEFAULT_USER_TEMPLATE = `# USER.md - User Profile
|
||||||
|
|
||||||
|
- Name:
|
||||||
|
- Preferred address:
|
||||||
|
- Pronouns (optional):
|
||||||
|
- Timezone (optional):
|
||||||
|
- Notes:
|
||||||
|
`;
|
||||||
|
|
||||||
const TEMPLATE_DIR = path.resolve(
|
const TEMPLATE_DIR = path.resolve(
|
||||||
path.dirname(fileURLToPath(import.meta.url)),
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
"../../docs/templates",
|
"../../docs/templates",
|
||||||
@@ -92,7 +160,10 @@ async function loadTemplate(name: string, fallback: string): Promise<string> {
|
|||||||
export type WorkspaceBootstrapFileName =
|
export type WorkspaceBootstrapFileName =
|
||||||
| typeof DEFAULT_AGENTS_FILENAME
|
| typeof DEFAULT_AGENTS_FILENAME
|
||||||
| typeof DEFAULT_SOUL_FILENAME
|
| typeof DEFAULT_SOUL_FILENAME
|
||||||
| typeof DEFAULT_TOOLS_FILENAME;
|
| typeof DEFAULT_TOOLS_FILENAME
|
||||||
|
| typeof DEFAULT_IDENTITY_FILENAME
|
||||||
|
| typeof DEFAULT_USER_FILENAME
|
||||||
|
| typeof DEFAULT_BOOTSTRAP_FILENAME;
|
||||||
|
|
||||||
export type WorkspaceBootstrapFile = {
|
export type WorkspaceBootstrapFile = {
|
||||||
name: WorkspaceBootstrapFileName;
|
name: WorkspaceBootstrapFileName;
|
||||||
@@ -121,6 +192,9 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
agentsPath?: string;
|
agentsPath?: string;
|
||||||
soulPath?: string;
|
soulPath?: string;
|
||||||
toolsPath?: string;
|
toolsPath?: string;
|
||||||
|
identityPath?: string;
|
||||||
|
userPath?: string;
|
||||||
|
bootstrapPath?: string;
|
||||||
}> {
|
}> {
|
||||||
const rawDir = params?.dir?.trim()
|
const rawDir = params?.dir?.trim()
|
||||||
? params.dir.trim()
|
? params.dir.trim()
|
||||||
@@ -133,6 +207,9 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
const agentsPath = path.join(dir, DEFAULT_AGENTS_FILENAME);
|
const agentsPath = path.join(dir, DEFAULT_AGENTS_FILENAME);
|
||||||
const soulPath = path.join(dir, DEFAULT_SOUL_FILENAME);
|
const soulPath = path.join(dir, DEFAULT_SOUL_FILENAME);
|
||||||
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
||||||
|
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
||||||
|
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
||||||
|
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
||||||
|
|
||||||
const agentsTemplate = await loadTemplate(
|
const agentsTemplate = await loadTemplate(
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
@@ -146,12 +223,35 @@ export async function ensureAgentWorkspace(params?: {
|
|||||||
DEFAULT_TOOLS_FILENAME,
|
DEFAULT_TOOLS_FILENAME,
|
||||||
DEFAULT_TOOLS_TEMPLATE,
|
DEFAULT_TOOLS_TEMPLATE,
|
||||||
);
|
);
|
||||||
|
const identityTemplate = await loadTemplate(
|
||||||
|
DEFAULT_IDENTITY_FILENAME,
|
||||||
|
DEFAULT_IDENTITY_TEMPLATE,
|
||||||
|
);
|
||||||
|
const userTemplate = await loadTemplate(
|
||||||
|
DEFAULT_USER_FILENAME,
|
||||||
|
DEFAULT_USER_TEMPLATE,
|
||||||
|
);
|
||||||
|
const bootstrapTemplate = await loadTemplate(
|
||||||
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
DEFAULT_BOOTSTRAP_TEMPLATE,
|
||||||
|
);
|
||||||
|
|
||||||
await writeFileIfMissing(agentsPath, agentsTemplate);
|
await writeFileIfMissing(agentsPath, agentsTemplate);
|
||||||
await writeFileIfMissing(soulPath, soulTemplate);
|
await writeFileIfMissing(soulPath, soulTemplate);
|
||||||
await writeFileIfMissing(toolsPath, toolsTemplate);
|
await writeFileIfMissing(toolsPath, toolsTemplate);
|
||||||
|
await writeFileIfMissing(identityPath, identityTemplate);
|
||||||
|
await writeFileIfMissing(userPath, userTemplate);
|
||||||
|
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
||||||
|
|
||||||
return { dir, agentsPath, soulPath, toolsPath };
|
return {
|
||||||
|
dir,
|
||||||
|
agentsPath,
|
||||||
|
soulPath,
|
||||||
|
toolsPath,
|
||||||
|
identityPath,
|
||||||
|
userPath,
|
||||||
|
bootstrapPath,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadWorkspaceBootstrapFiles(
|
export async function loadWorkspaceBootstrapFiles(
|
||||||
@@ -175,6 +275,18 @@ export async function loadWorkspaceBootstrapFiles(
|
|||||||
name: DEFAULT_TOOLS_FILENAME,
|
name: DEFAULT_TOOLS_FILENAME,
|
||||||
filePath: path.join(resolvedDir, DEFAULT_TOOLS_FILENAME),
|
filePath: path.join(resolvedDir, DEFAULT_TOOLS_FILENAME),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: DEFAULT_IDENTITY_FILENAME,
|
||||||
|
filePath: path.join(resolvedDir, DEFAULT_IDENTITY_FILENAME),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: DEFAULT_USER_FILENAME,
|
||||||
|
filePath: path.join(resolvedDir, DEFAULT_USER_FILENAME),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result: WorkspaceBootstrapFile[] = [];
|
const result: WorkspaceBootstrapFile[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user