Skip to content
Closed
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
7 changes: 0 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@
# Copy this file to .env and fill in your values:
# cp .env.example .env

# ========================
# REQUIRED
# ========================

# Your Anthropic API key (starts with sk-ant-)
ANTHROPIC_API_KEY=

# ========================
# OPTIONAL: Slack
# ========================
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ MCP flow: External client -> /mcp endpoint -> bearer auth -> MCP Server -> tool
**Docker (recommended for new installs):**
```bash
git clone https://github.com/ghostwright/phantom.git && cd phantom
cp .env.example .env # add ANTHROPIC_API_KEY + Slack tokens
cp .env.example .env # add Slack tokens (auth handled by Agent SDK)
docker compose up -d
```

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ This is what happens when you give an AI its own computer.
```bash
curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/docker-compose.user.yaml -o docker-compose.yaml
curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/.env.example -o .env
# Edit .env - add your ANTHROPIC_API_KEY, Slack tokens, and OWNER_SLACK_USER_ID
# Edit .env - add your Slack tokens and OWNER_SLACK_USER_ID
docker compose up -d
```

Expand Down Expand Up @@ -294,7 +294,7 @@ docker exec phantom-ollama ollama pull nomic-embed-text
bun run phantom init --yes

# Set your API key
export ANTHROPIC_API_KEY=sk-ant-...
# Auth is handled by the Claude Agent SDK (OAuth/Claude MAX) — no API key needed

# Start
bun run phantom start
Expand Down
300 changes: 292 additions & 8 deletions bun.lock

Large diffs are not rendered by default.

19 changes: 7 additions & 12 deletions config/memory.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
# Environment variables QDRANT_URL and OLLAMA_URL override these values.
# In Docker, compose sets QDRANT_URL=http://qdrant:6333 and OLLAMA_URL=http://ollama:11434.
qdrant:
url: "http://localhost:6333"

ollama:
url: "http://localhost:11434"
model: "nomic-embed-text"
# CLAWMEM_STORE_PATH and CLAWMEM_EMBED_MODEL override these values.
# Optional remote embeddings are configured via CLAWMEM_EMBED_URL and
# CLAWMEM_EMBED_API_KEY; ClawMem reads those directly from the environment.
clawmem:
store_path: "data/clawmem.sqlite"
embed_model: "embedding"
busy_timeout_ms: 5000

collections:
episodes: "episodes"
semantic_facts: "semantic_facts"
procedures: "procedures"

embedding:
dimensions: 768
batch_size: 32

context:
max_tokens: 50000
episode_limit: 10
Expand Down
1 change: 0 additions & 1 deletion docs/deploy-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ Format: `U` followed by alphanumeric characters (e.g., `UKWMQ41F0`)
Create a file at `.env.<name>` in the Phantom repo root:

```
ANTHROPIC_API_KEY=sk-ant-your-api-key
SLACK_BOT_TOKEN=xoxb-their-bot-token
SLACK_APP_TOKEN=xapp-their-app-token
OWNER_SLACK_USER_ID=UTHEIR_USER_ID
Expand Down
1 change: 0 additions & 1 deletion docs/docker-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ What it does (5 steps, all idempotent):
Create `.env.<name>` in the Phantom repo root with:

```
ANTHROPIC_API_KEY=sk-ant-...
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
OWNER_SLACK_USER_ID=U04ABC123
Expand Down
12 changes: 3 additions & 9 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,9 @@ curl -fsSL https://raw.githubusercontent.com/ghostwright/phantom/main/.env.examp

Open `.env` in your editor and fill in these values:

### Required
### Authentication

```
ANTHROPIC_API_KEY=sk-ant-your-key-here
```

Your Anthropic API key. This is the only value you absolutely must set.
Auth is handled automatically by the Claude Agent SDK (OAuth / Claude MAX). No API key is required.

### Slack (recommended)

Expand Down Expand Up @@ -105,13 +101,12 @@ Everything else in `.env.example` has sensible defaults. You can leave the rest
If you just want the shortest path to a running Phantom with Slack:

```
ANTHROPIC_API_KEY=sk-ant-your-key-here
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
OWNER_SLACK_USER_ID=U04ABC123XY
```

Four lines. That is all Phantom needs.
Three lines. That is all Phantom needs. Auth is handled by the Agent SDK.

## Step 3: Start Phantom

Expand Down Expand Up @@ -209,7 +204,6 @@ Create your `.env` file:

```bash
cat > .env << 'EOF'
ANTHROPIC_API_KEY=sk-ant-your-key-here
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
OWNER_SLACK_USER_ID=U04ABC123XY
Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ These are configured automatically by the [app manifest](../slack-app-manifest.y

| Variable | Required | Purpose |
|----------|----------|---------|
| `ANTHROPIC_API_KEY` | Yes | Claude Opus 4.6 API access |
| ~~`ANTHROPIC_API_KEY`~~ | No | Removed — auth handled by Claude Agent SDK (OAuth) |
| `SLACK_BOT_TOKEN` | For Slack | Bot user OAuth token (`xoxb-`) |
| `SLACK_APP_TOKEN` | For Slack | App-level token for Socket Mode (`xapp-`) |
| `SLACK_CHANNEL_ID` | For Slack | Default channel for intro message on first start |
Expand Down
2 changes: 1 addition & 1 deletion docs/self-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ If metrics degrade after an evolution (success rate drops, correction rate incre

## LLM Judges

When the `ANTHROPIC_API_KEY` is available, 6 LLM judges provide higher-quality evolution:
The Agent SDK powers 6 LLM judges that provide higher-quality evolution:

| Judge | Model | Strategy | Purpose |
|-------|-------|----------|---------|
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.77",
"@anthropic-ai/sdk": "^0.80.0",
"@modelcontextprotocol/sdk": "^1.28.0",
"@slack/bolt": "^4.6.0",
"clawmem": "file:../ClawMem",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use a resolvable ClawMem dependency source

Pointing clawmem to file:../ClawMem makes installs fail for a normal clone/CI checkout where no sibling ../ClawMem repo exists, which in turn breaks runtime imports like clawmem/src/llm.ts and prevents tests/startup from running. This effectively turns the project into a non-self-contained build unless every environment is manually prepared with that external path.

Useful? React with 👍 / 👎.

"croner": "^10.0.1",
"imapflow": "^1.2.18",
"nodemailer": "^8.0.4",
Expand Down
4 changes: 2 additions & 2 deletions src/agent/__tests__/prompt-assembler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ describe("assemblePrompt Docker awareness", () => {
expect(prompt).toContain("sibling");
expect(prompt).toContain("ClickHouse, Postgres, Redis");
expect(prompt).toContain("Docker volumes");
expect(prompt).toContain("http://qdrant:6333");
expect(prompt).toContain("http://ollama:11434");
expect(prompt).toContain("ClawMem");
expect(prompt).toContain("CLAWMEM_EMBED_URL");
});

test("Docker mode warns agent not to modify compose/Dockerfile", () => {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/prompt-assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ function buildEnvironment(config: PhantomConfig): string {
lines.push("- Your data (config, memory, web pages, repos) persists in Docker volumes.");
lines.push("- To connect to services you create, use their container name as the hostname.");
lines.push("- Do NOT modify docker-compose.yaml or Dockerfile. Those are managed by the operator.");
lines.push("- Qdrant is at http://qdrant:6333, Ollama is at http://ollama:11434.");
lines.push("- Persistent memory runs on ClawMem with a local SQLite store in the mounted data volume.");
lines.push("- If remote embeddings are configured, they are provided through CLAWMEM_EMBED_URL.");
}

return lines.join("\n");
Expand Down
1 change: 1 addition & 0 deletions src/cli/__tests__/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("phantom doctor", () => {
expect(logs.some((l) => l.includes("Phantom Doctor"))).toBe(true);
expect(logs.some((l) => l.includes("Bun"))).toBe(true);
expect(logs.some((l) => l.includes("Docker"))).toBe(true);
expect(logs.some((l) => l.includes("ClawMem"))).toBe(true);
expect(logs.some((l) => l.includes("Config"))).toBe(true);
expect(logs.some((l) => l.includes("SQLite"))).toBe(true);
});
Expand Down
58 changes: 20 additions & 38 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import { parseArgs } from "node:util";
import { createClawMemStore } from "../memory/clawmem-runtime.ts";

type CheckResult = {
name: string;
Expand Down Expand Up @@ -39,47 +40,29 @@ async function checkDocker(): Promise<CheckResult> {
}
}

async function checkQdrant(): Promise<CheckResult> {
async function checkClawMem(): Promise<CheckResult> {
try {
const resp = await fetch("http://localhost:6333/healthz", { signal: AbortSignal.timeout(3000) });
if (resp.ok) {
return { name: "Qdrant", status: "ok", message: "Healthy (port 6333)" };
}
return { name: "Qdrant", status: "fail", message: `HTTP ${resp.status}`, fix: "docker compose up -d qdrant" };
} catch {
const [{ loadMemoryConfig }] = await Promise.all([import("../memory/config.ts")]);
const config = loadMemoryConfig();
const store = await createClawMemStore(config.clawmem.store_path, {
busyTimeout: config.clawmem.busy_timeout_ms,
});
const status = store.getStatus();
store.close();

const vectorState = status.hasVectorIndex ? "vectors ready" : "FTS-only until first embeddings";
return {
name: "Qdrant",
status: "fail",
message: "Not reachable at localhost:6333",
fix: "docker compose up -d qdrant",
name: "ClawMem",
status: "ok",
message: `${config.clawmem.store_path} (${status.totalDocuments} docs, ${vectorState})`,
};
}
}

async function checkOllama(): Promise<CheckResult> {
try {
const resp = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
if (!resp.ok) {
return { name: "Ollama", status: "fail", message: `HTTP ${resp.status}`, fix: "docker compose up -d ollama" };
}
const data = (await resp.json()) as { models?: Array<{ name: string }> };
const models = data.models ?? [];
const hasEmbed = models.some((m) => m.name.includes("nomic-embed-text"));
if (!hasEmbed) {
return {
name: "Ollama",
status: "warn",
message: "Running but nomic-embed-text model not pulled",
fix: "docker exec phantom-ollama ollama pull nomic-embed-text",
};
}
return { name: "Ollama", status: "ok", message: `Healthy, ${models.length} model(s) loaded` };
} catch {
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return {
name: "Ollama",
name: "ClawMem",
status: "fail",
message: "Not reachable at localhost:11434",
fix: "docker compose up -d ollama",
message: msg,
fix: "Check config/memory.yaml and ensure the local ClawMem dependency is installed",
};
}
}
Expand Down Expand Up @@ -188,8 +171,7 @@ export async function runDoctor(args: string[]): Promise<void> {
const checks = await Promise.all([
checkBun(),
checkDocker(),
checkQdrant(),
checkOllama(),
checkClawMem(),
checkConfig(),
checkMcpConfig(),
checkDatabase(),
Expand Down
5 changes: 2 additions & 3 deletions src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,8 @@ export async function runInit(args: string[]): Promise<void> {
}

console.log("\nNext steps:");
console.log(" 1. Set ANTHROPIC_API_KEY in your environment");
console.log(" 2. Start Docker services: docker compose up -d");
console.log(" 3. Start Phantom: phantom start");
console.log(" 1. Start Docker services: docker compose up -d");
console.log(" 2. Start Phantom: phantom start");
console.log(" 4. Connect from Claude Code:");
console.log(` claude mcp add phantom -- curl -H "Authorization: Bearer ${mcp.adminToken}" https://your-host/mcp`);
}
5 changes: 2 additions & 3 deletions src/cli/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type HealthResponse = {
agent: string;
role: { id: string; name: string };
channels: Record<string, boolean>;
memory: { qdrant: boolean; ollama: boolean };
memory: { clawmem: boolean; configured: boolean };
evolution: { generation: number };
onboarding?: string;
peers?: Record<string, { healthy: boolean; latencyMs: number; error?: string }>;
Expand Down Expand Up @@ -71,8 +71,7 @@ export async function runStatus(args: string[]): Promise<void> {
.map(([name]) => name);

const channelStr = channelList.length > 0 ? channelList.join(", ") : "none";
const memoryStr =
data.memory.qdrant && data.memory.ollama ? "ok" : data.memory.qdrant || data.memory.ollama ? "degraded" : "offline";
const memoryStr = data.memory.clawmem ? "ok" : data.memory.configured ? "offline" : "disabled";

console.log(
`${data.agent} | ${data.role.name} | v${data.version} | ` +
Expand Down
18 changes: 4 additions & 14 deletions src/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,11 @@ export const ChannelsConfigSchema = z.object({
export type ChannelsConfig = z.infer<typeof ChannelsConfigSchema>;

export const MemoryConfigSchema = z.object({
qdrant: z
clawmem: z
.object({
url: z.string().url().default("http://localhost:6333"),
})
.default({}),
ollama: z
.object({
url: z.string().url().default("http://localhost:11434"),
model: z.string().min(1).default("nomic-embed-text"),
store_path: z.string().min(1).default("data/clawmem.sqlite"),
embed_model: z.string().min(1).default("embedding"),
busy_timeout_ms: z.number().int().nonnegative().default(5000),
})
.default({}),
collections: z
Expand All @@ -88,12 +84,6 @@ export const MemoryConfigSchema = z.object({
procedures: z.string().min(1).default("procedures"),
})
.default({}),
embedding: z
.object({
dimensions: z.number().int().positive().default(768),
batch_size: z.number().int().positive().default(32),
})
.default({}),
context: z
.object({
max_tokens: z.number().int().positive().default(50000),
Expand Down
24 changes: 7 additions & 17 deletions src/core/__tests__/health-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,19 @@ import type { MemoryHealth } from "../../memory/types.ts";
* This must mirror the logic in startServer's /health handler exactly.
*/
function computeHealthStatus(memory: MemoryHealth): string {
const allHealthy = memory.qdrant && memory.ollama;
const someHealthy = memory.qdrant || memory.ollama;
return allHealthy ? "ok" : someHealthy ? "degraded" : memory.configured ? "down" : "ok";
return memory.clawmem ? "ok" : memory.configured ? "down" : "ok";
}

describe("health status logic", () => {
test("both healthy and configured -> ok", () => {
expect(computeHealthStatus({ qdrant: true, ollama: true, configured: true })).toBe("ok");
test("clawmem healthy and configured -> ok", () => {
expect(computeHealthStatus({ clawmem: true, configured: true })).toBe("ok");
});

test("qdrant up, ollama down, configured -> degraded", () => {
expect(computeHealthStatus({ qdrant: true, ollama: false, configured: true })).toBe("degraded");
test("clawmem down when configured -> down", () => {
expect(computeHealthStatus({ clawmem: false, configured: true })).toBe("down");
});

test("qdrant down, ollama up, configured -> degraded", () => {
expect(computeHealthStatus({ qdrant: false, ollama: true, configured: true })).toBe("degraded");
});

test("both down when configured -> down (the bug fix)", () => {
expect(computeHealthStatus({ qdrant: false, ollama: false, configured: true })).toBe("down");
});

test("both down when not configured -> ok (memory intentionally not set up)", () => {
expect(computeHealthStatus({ qdrant: false, ollama: false, configured: false })).toBe("ok");
test("clawmem down when not configured -> ok", () => {
expect(computeHealthStatus({ clawmem: false, configured: false })).toBe("ok");
});
});
7 changes: 2 additions & 5 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,11 @@ export function startServer(config: PhantomConfig, startedAt: number): ReturnTyp
if (url.pathname === "/health") {
const memory: MemoryHealth = memoryHealthProvider
? await memoryHealthProvider()
: { qdrant: false, ollama: false, configured: false };
: { clawmem: false, configured: false };

const channels: Record<string, boolean> = channelHealthProvider ? channelHealthProvider() : {};

const allHealthy = memory.qdrant && memory.ollama;
const someHealthy = memory.qdrant || memory.ollama;
// Both up -> ok. One up -> degraded. Both down + configured -> down. Not configured -> ok.
const status = allHealthy ? "ok" : someHealthy ? "degraded" : memory.configured ? "down" : "ok";
const status = memory.clawmem ? "ok" : memory.configured ? "down" : "ok";
const evolutionGeneration = evolutionVersionProvider ? evolutionVersionProvider() : 0;

const roleInfo = roleInfoProvider ? roleInfoProvider() : null;
Expand Down
Loading