Skip to content

Commit 9a0e3a8

Browse files
authored
Merge pull request #252 from paperclipai/dotta
Dotta updates - sorry it's so large
2 parents 1afadd7 + 1c1b86f commit 9a0e3a8

62 files changed

Lines changed: 5595 additions & 384 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli/esbuild.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const workspacePaths = [
2222
"packages/adapters/claude-local",
2323
"packages/adapters/codex-local",
2424
"packages/adapters/openclaw",
25+
"packages/adapters/openclaw-gateway",
2526
];
2627

2728
// Workspace packages that should NOT be bundled — they'll be published

cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@paperclipai/adapter-opencode-local": "workspace:*",
4141
"@paperclipai/adapter-pi-local": "workspace:*",
4242
"@paperclipai/adapter-openclaw": "workspace:*",
43+
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
4344
"@paperclipai/adapter-utils": "workspace:*",
4445
"@paperclipai/db": "workspace:*",
4546
"@paperclipai/server": "workspace:*",

cli/src/adapters/registry.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
55
import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli";
66
import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli";
77
import { printOpenClawStreamEvent } from "@paperclipai/adapter-openclaw/cli";
8+
import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-gateway/cli";
89
import { processCLIAdapter } from "./process/index.js";
910
import { httpCLIAdapter } from "./http/index.js";
1011

@@ -38,8 +39,23 @@ const openclawCLIAdapter: CLIAdapterModule = {
3839
formatStdoutEvent: printOpenClawStreamEvent,
3940
};
4041

42+
const openclawGatewayCLIAdapter: CLIAdapterModule = {
43+
type: "openclaw_gateway",
44+
formatStdoutEvent: printOpenClawGatewayStreamEvent,
45+
};
46+
4147
const adaptersByType = new Map<string, CLIAdapterModule>(
42-
[claudeLocalCLIAdapter, codexLocalCLIAdapter, openCodeLocalCLIAdapter, piLocalCLIAdapter, cursorLocalCLIAdapter, openclawCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
48+
[
49+
claudeLocalCLIAdapter,
50+
codexLocalCLIAdapter,
51+
openCodeLocalCLIAdapter,
52+
piLocalCLIAdapter,
53+
cursorLocalCLIAdapter,
54+
openclawCLIAdapter,
55+
openclawGatewayCLIAdapter,
56+
processCLIAdapter,
57+
httpCLIAdapter,
58+
].map((a) => [a.type, a]),
4359
);
4460

4561
export function getCLIAdapter(type: string): CLIAdapterModule {

cli/src/commands/client/agent.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Command } from "commander";
22
import type { Agent } from "@paperclipai/shared";
3+
import fs from "node:fs/promises";
4+
import os from "node:os";
5+
import path from "node:path";
6+
import { fileURLToPath } from "node:url";
37
import {
48
addCommonClientOptions,
59
formatInlineRecord,
@@ -13,6 +17,107 @@ interface AgentListOptions extends BaseClientOptions {
1317
companyId?: string;
1418
}
1519

20+
interface AgentLocalCliOptions extends BaseClientOptions {
21+
companyId?: string;
22+
keyName?: string;
23+
installSkills?: boolean;
24+
}
25+
26+
interface CreatedAgentKey {
27+
id: string;
28+
name: string;
29+
token: string;
30+
createdAt: string;
31+
}
32+
33+
interface SkillsInstallSummary {
34+
tool: "codex" | "claude";
35+
target: string;
36+
linked: string[];
37+
skipped: string[];
38+
failed: Array<{ name: string; error: string }>;
39+
}
40+
41+
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
42+
const PAPERCLIP_SKILLS_CANDIDATES = [
43+
path.resolve(__moduleDir, "../../../../../skills"), // dev: cli/src/commands/client -> repo root/skills
44+
path.resolve(process.cwd(), "skills"),
45+
];
46+
47+
function codexSkillsHome(): string {
48+
const fromEnv = process.env.CODEX_HOME?.trim();
49+
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".codex");
50+
return path.join(base, "skills");
51+
}
52+
53+
function claudeSkillsHome(): string {
54+
const fromEnv = process.env.CLAUDE_HOME?.trim();
55+
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".claude");
56+
return path.join(base, "skills");
57+
}
58+
59+
async function resolvePaperclipSkillsDir(): Promise<string | null> {
60+
for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) {
61+
const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false);
62+
if (isDir) return candidate;
63+
}
64+
return null;
65+
}
66+
67+
async function installSkillsForTarget(
68+
sourceSkillsDir: string,
69+
targetSkillsDir: string,
70+
tool: "codex" | "claude",
71+
): Promise<SkillsInstallSummary> {
72+
const summary: SkillsInstallSummary = {
73+
tool,
74+
target: targetSkillsDir,
75+
linked: [],
76+
skipped: [],
77+
failed: [],
78+
};
79+
80+
await fs.mkdir(targetSkillsDir, { recursive: true });
81+
const entries = await fs.readdir(sourceSkillsDir, { withFileTypes: true });
82+
for (const entry of entries) {
83+
if (!entry.isDirectory()) continue;
84+
const source = path.join(sourceSkillsDir, entry.name);
85+
const target = path.join(targetSkillsDir, entry.name);
86+
const existing = await fs.lstat(target).catch(() => null);
87+
if (existing) {
88+
summary.skipped.push(entry.name);
89+
continue;
90+
}
91+
92+
try {
93+
await fs.symlink(source, target);
94+
summary.linked.push(entry.name);
95+
} catch (err) {
96+
summary.failed.push({
97+
name: entry.name,
98+
error: err instanceof Error ? err.message : String(err),
99+
});
100+
}
101+
}
102+
103+
return summary;
104+
}
105+
106+
function buildAgentEnvExports(input: {
107+
apiBase: string;
108+
companyId: string;
109+
agentId: string;
110+
apiKey: string;
111+
}): string {
112+
const escaped = (value: string) => value.replace(/'/g, "'\"'\"'");
113+
return [
114+
`export PAPERCLIP_API_URL='${escaped(input.apiBase)}'`,
115+
`export PAPERCLIP_COMPANY_ID='${escaped(input.companyId)}'`,
116+
`export PAPERCLIP_AGENT_ID='${escaped(input.agentId)}'`,
117+
`export PAPERCLIP_API_KEY='${escaped(input.apiKey)}'`,
118+
].join("\n");
119+
}
120+
16121
export function registerAgentCommands(program: Command): void {
17122
const agent = program.command("agent").description("Agent operations");
18123

@@ -71,4 +176,96 @@ export function registerAgentCommands(program: Command): void {
71176
}
72177
}),
73178
);
179+
180+
addCommonClientOptions(
181+
agent
182+
.command("local-cli")
183+
.description(
184+
"Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports",
185+
)
186+
.argument("<agentRef>", "Agent ID or shortname/url-key")
187+
.requiredOption("-C, --company-id <id>", "Company ID")
188+
.option("--key-name <name>", "API key label", "local-cli")
189+
.option(
190+
"--no-install-skills",
191+
"Skip installing Paperclip skills into ~/.codex/skills and ~/.claude/skills",
192+
)
193+
.action(async (agentRef: string, opts: AgentLocalCliOptions) => {
194+
try {
195+
const ctx = resolveCommandContext(opts, { requireCompany: true });
196+
const query = new URLSearchParams({ companyId: ctx.companyId ?? "" });
197+
const agentRow = await ctx.api.get<Agent>(
198+
`/api/agents/${encodeURIComponent(agentRef)}?${query.toString()}`,
199+
);
200+
201+
const now = new Date().toISOString().replaceAll(":", "-");
202+
const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;
203+
const key = await ctx.api.post<CreatedAgentKey>(`/api/agents/${agentRow.id}/keys`, { name: keyName });
204+
205+
const installSummaries: SkillsInstallSummary[] = [];
206+
if (opts.installSkills !== false) {
207+
const skillsDir = await resolvePaperclipSkillsDir();
208+
if (!skillsDir) {
209+
throw new Error(
210+
"Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.",
211+
);
212+
}
213+
214+
installSummaries.push(
215+
await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"),
216+
await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"),
217+
);
218+
}
219+
220+
const exportsText = buildAgentEnvExports({
221+
apiBase: ctx.api.apiBase,
222+
companyId: agentRow.companyId,
223+
agentId: agentRow.id,
224+
apiKey: key.token,
225+
});
226+
227+
if (ctx.json) {
228+
printOutput(
229+
{
230+
agent: {
231+
id: agentRow.id,
232+
name: agentRow.name,
233+
urlKey: agentRow.urlKey,
234+
companyId: agentRow.companyId,
235+
},
236+
key: {
237+
id: key.id,
238+
name: key.name,
239+
createdAt: key.createdAt,
240+
token: key.token,
241+
},
242+
skills: installSummaries,
243+
exports: exportsText,
244+
},
245+
{ json: true },
246+
);
247+
return;
248+
}
249+
250+
console.log(`Agent: ${agentRow.name} (${agentRow.id})`);
251+
console.log(`API key created: ${key.name} (${key.id})`);
252+
if (installSummaries.length > 0) {
253+
for (const summary of installSummaries) {
254+
console.log(
255+
`${summary.tool}: linked=${summary.linked.length} skipped=${summary.skipped.length} failed=${summary.failed.length} target=${summary.target}`,
256+
);
257+
for (const failed of summary.failed) {
258+
console.log(` failed ${failed.name}: ${failed.error}`);
259+
}
260+
}
261+
}
262+
console.log("");
263+
console.log("# Run this in your shell before launching codex/claude:");
264+
console.log(exportsText);
265+
} catch (err) {
266+
handleCommandError(err);
267+
}
268+
}),
269+
{ includeCompany: false },
270+
);
74271
}

doc/CLI.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ pnpm paperclipai issue release <issue-id>
116116
```sh
117117
pnpm paperclipai agent list --company-id <company-id>
118118
pnpm paperclipai agent get <agent-id>
119+
pnpm paperclipai agent local-cli <agent-id-or-shortname> --company-id <company-id>
120+
```
121+
122+
`agent local-cli` is the quickest way to run local Claude/Codex manually as a Paperclip agent:
123+
124+
- creates a new long-lived agent API key
125+
- installs missing Paperclip skills into `~/.codex/skills` and `~/.claude/skills`
126+
- prints `export ...` lines for `PAPERCLIP_API_URL`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_AGENT_ID`, and `PAPERCLIP_API_KEY`
127+
128+
Example for shortname-based local setup:
129+
130+
```sh
131+
pnpm paperclipai agent local-cli codexcoder --company-id <company-id>
132+
pnpm paperclipai agent local-cli claudecoder --company-id <company-id>
119133
```
120134

121135
## Approval Commands

doc/OPENCLAW_ONBOARDING.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
Use this exact checklist.
2+
3+
1. Start Paperclip in auth mode.
4+
```bash
5+
cd <paperclip-repo-root>
6+
pnpm dev --tailscale-auth
7+
```
8+
Then verify:
9+
```bash
10+
curl -sS http://127.0.0.1:3100/api/health | jq
11+
```
12+
13+
2. Start a clean/stock OpenClaw Docker.
14+
```bash
15+
OPENCLAW_RESET_STATE=1 OPENCLAW_BUILD=1 ./scripts/smoke/openclaw-docker-ui.sh
16+
```
17+
Open the printed `Dashboard URL` (includes `#token=...`) in your browser.
18+
19+
3. In Paperclip UI, go to `http://127.0.0.1:3100/CLA/company/settings`.
20+
21+
4. Use the agent snippet flow.
22+
- Copy the snippet from company settings.
23+
- Paste it into OpenClaw main chat as one message.
24+
- If it stalls, send one follow-up: `How is onboarding going? Continue setup now.`
25+
26+
5. Approve the join request in Paperclip UI, then confirm the OpenClaw agent appears in CLA agents.
27+
28+
6. Case A (manual issue test).
29+
- Create an issue assigned to the OpenClaw agent.
30+
- Put instructions: “post comment `OPENCLAW_CASE_A_OK_<timestamp>` and mark done.”
31+
- Verify in UI: issue status becomes `done` and comment exists.
32+
33+
7. Case B (message tool test).
34+
- Create another issue assigned to OpenClaw.
35+
- Instructions: “send `OPENCLAW_CASE_B_OK_<timestamp>` to main webchat via message tool, then comment same marker on issue, then mark done.”
36+
- Verify both:
37+
- marker comment on issue
38+
- marker text appears in OpenClaw main chat
39+
40+
8. Case C (new session memory/skills test).
41+
- In OpenClaw, start `/new` session.
42+
- Ask it to create a new CLA issue in Paperclip with unique title `OPENCLAW_CASE_C_CREATED_<timestamp>`.
43+
- Verify in Paperclip UI that new issue exists.
44+
45+
9. Watch logs during test (optional but helpful):
46+
```bash
47+
docker compose -f /tmp/openclaw-docker/docker-compose.yml -f /tmp/openclaw-docker/.paperclip-openclaw.override.yml logs -f openclaw-gateway
48+
```
49+
50+
10. Expected pass criteria.
51+
- Case A: `done` + marker comment.
52+
- Case B: `done` + marker comment + main-chat message visible.
53+
- Case C: original task done and new issue created from `/new` session.
54+
55+
If you want, I can also give you a single “observer mode” command that runs the stock smoke harness while you watch the same steps live in UI.

docs/adapters/claude-local.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ If resume fails with an unknown session error, the adapter automatically retries
4747

4848
The adapter creates a temporary directory with symlinks to Paperclip skills and passes it via `--add-dir`. This makes skills discoverable without polluting the agent's working directory.
4949

50+
For manual local CLI usage outside heartbeat runs (for example running as `claudecoder` directly), use:
51+
52+
```sh
53+
pnpm paperclipai agent local-cli claudecoder --company-id <company-id>
54+
```
55+
56+
This installs Paperclip skills in `~/.claude/skills`, creates an agent API key, and prints shell exports to run as that agent.
57+
5058
## Environment Test
5159

5260
Use the "Test Environment" button in the UI to validate the adapter config. It checks:

docs/adapters/codex-local.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ Codex uses `previous_response_id` for session continuity. The adapter serializes
3030

3131
The adapter symlinks Paperclip skills into the global Codex skills directory (`~/.codex/skills`). Existing user skills are not overwritten.
3232

33+
For manual local CLI usage outside heartbeat runs (for example running as `codexcoder` directly), use:
34+
35+
```sh
36+
pnpm paperclipai agent local-cli codexcoder --company-id <company-id>
37+
```
38+
39+
This installs any missing skills, creates an agent API key, and prints shell exports to run as that agent.
40+
3341
## Environment Test
3442

3543
The environment test checks:

0 commit comments

Comments
 (0)