offwatch 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -178
- package/bin/offwatch.js +6 -7
- package/lib/downloader.js +112 -0
- package/package.json +18 -11
- package/postinstall.js +21 -0
- package/src/__tests__/agent-jwt-env.test.ts +0 -79
- package/src/__tests__/allowed-hostname.test.ts +0 -80
- package/src/__tests__/auth-command-registration.test.ts +0 -16
- package/src/__tests__/board-auth.test.ts +0 -53
- package/src/__tests__/common.test.ts +0 -98
- package/src/__tests__/company-delete.test.ts +0 -95
- package/src/__tests__/company-import-export-e2e.test.ts +0 -502
- package/src/__tests__/company-import-url.test.ts +0 -74
- package/src/__tests__/company-import-zip.test.ts +0 -44
- package/src/__tests__/company.test.ts +0 -599
- package/src/__tests__/context.test.ts +0 -70
- package/src/__tests__/data-dir.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -102
- package/src/__tests__/feedback.test.ts +0 -177
- package/src/__tests__/helpers/embedded-postgres.ts +0 -6
- package/src/__tests__/helpers/zip.ts +0 -87
- package/src/__tests__/home-paths.test.ts +0 -44
- package/src/__tests__/http.test.ts +0 -106
- package/src/__tests__/network-bind.test.ts +0 -62
- package/src/__tests__/onboard.test.ts +0 -166
- package/src/__tests__/routines.test.ts +0 -249
- package/src/__tests__/telemetry.test.ts +0 -117
- package/src/__tests__/worktree-merge-history.test.ts +0 -492
- package/src/__tests__/worktree.test.ts +0 -982
- package/src/adapters/http/format-event.ts +0 -4
- package/src/adapters/http/index.ts +0 -7
- package/src/adapters/index.ts +0 -2
- package/src/adapters/process/format-event.ts +0 -4
- package/src/adapters/process/index.ts +0 -7
- package/src/adapters/registry.ts +0 -63
- package/src/checks/agent-jwt-secret-check.ts +0 -40
- package/src/checks/config-check.ts +0 -33
- package/src/checks/database-check.ts +0 -59
- package/src/checks/deployment-auth-check.ts +0 -88
- package/src/checks/index.ts +0 -18
- package/src/checks/llm-check.ts +0 -82
- package/src/checks/log-check.ts +0 -30
- package/src/checks/path-resolver.ts +0 -1
- package/src/checks/port-check.ts +0 -24
- package/src/checks/secrets-check.ts +0 -146
- package/src/checks/storage-check.ts +0 -51
- package/src/client/board-auth.ts +0 -282
- package/src/client/command-label.ts +0 -4
- package/src/client/context.ts +0 -175
- package/src/client/http.ts +0 -255
- package/src/commands/allowed-hostname.ts +0 -40
- package/src/commands/auth-bootstrap-ceo.ts +0 -138
- package/src/commands/client/activity.ts +0 -71
- package/src/commands/client/agent.ts +0 -315
- package/src/commands/client/approval.ts +0 -259
- package/src/commands/client/auth.ts +0 -113
- package/src/commands/client/common.ts +0 -221
- package/src/commands/client/company.ts +0 -1578
- package/src/commands/client/context.ts +0 -125
- package/src/commands/client/dashboard.ts +0 -34
- package/src/commands/client/feedback.ts +0 -645
- package/src/commands/client/issue.ts +0 -411
- package/src/commands/client/plugin.ts +0 -374
- package/src/commands/client/zip.ts +0 -129
- package/src/commands/configure.ts +0 -201
- package/src/commands/db-backup.ts +0 -102
- package/src/commands/doctor.ts +0 -203
- package/src/commands/env.ts +0 -411
- package/src/commands/heartbeat-run.ts +0 -344
- package/src/commands/onboard.ts +0 -692
- package/src/commands/routines.ts +0 -352
- package/src/commands/run.ts +0 -216
- package/src/commands/worktree-lib.ts +0 -279
- package/src/commands/worktree-merge-history-lib.ts +0 -764
- package/src/commands/worktree.ts +0 -2876
- package/src/config/data-dir.ts +0 -48
- package/src/config/env.ts +0 -125
- package/src/config/home.ts +0 -80
- package/src/config/hostnames.ts +0 -26
- package/src/config/schema.ts +0 -30
- package/src/config/secrets-key.ts +0 -48
- package/src/config/server-bind.ts +0 -183
- package/src/config/store.ts +0 -120
- package/src/index.ts +0 -182
- package/src/prompts/database.ts +0 -157
- package/src/prompts/llm.ts +0 -43
- package/src/prompts/logging.ts +0 -37
- package/src/prompts/secrets.ts +0 -99
- package/src/prompts/server.ts +0 -221
- package/src/prompts/storage.ts +0 -146
- package/src/telemetry.ts +0 -49
- package/src/utils/banner.ts +0 -24
- package/src/utils/net.ts +0 -18
- package/src/utils/path-resolver.ts +0 -25
- package/src/version.ts +0 -10
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
|
-
import pc from "picocolors";
|
|
3
|
-
import { normalizeHostnameInput } from "../config/hostnames.js";
|
|
4
|
-
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
|
5
|
-
|
|
6
|
-
export async function addAllowedHostname(host: string, opts: { config?: string }): Promise<void> {
|
|
7
|
-
const configPath = resolveConfigPath(opts.config);
|
|
8
|
-
const config = readConfig(opts.config);
|
|
9
|
-
|
|
10
|
-
if (!config) {
|
|
11
|
-
p.log.error(`No config found at ${configPath}. Run ${pc.cyan("paperclip onboard")} first.`);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const normalized = normalizeHostnameInput(host);
|
|
16
|
-
const current = new Set((config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
|
|
17
|
-
const existed = current.has(normalized);
|
|
18
|
-
current.add(normalized);
|
|
19
|
-
|
|
20
|
-
config.server.allowedHostnames = Array.from(current).sort();
|
|
21
|
-
config.$meta.updatedAt = new Date().toISOString();
|
|
22
|
-
config.$meta.source = "configure";
|
|
23
|
-
writeConfig(config, opts.config);
|
|
24
|
-
|
|
25
|
-
if (existed) {
|
|
26
|
-
p.log.info(`Hostname ${pc.cyan(normalized)} is already allowed.`);
|
|
27
|
-
} else {
|
|
28
|
-
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
|
|
29
|
-
p.log.message(
|
|
30
|
-
pc.dim("Restart the Paperclip server for this change to take effect."),
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!(config.server.deploymentMode === "authenticated" && config.server.exposure === "private")) {
|
|
35
|
-
p.log.message(
|
|
36
|
-
pc.dim("Note: allowed hostnames are enforced only in authenticated/private mode."),
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import * as p from "@clack/prompts";
|
|
3
|
-
import pc from "picocolors";
|
|
4
|
-
import { and, eq, gt, isNull } from "drizzle-orm";
|
|
5
|
-
import { createDb, instanceUserRoles, invites } from "@paperclipai/db";
|
|
6
|
-
import { inferBindModeFromHost } from "@paperclipai/shared";
|
|
7
|
-
import { loadPaperclipEnvFile } from "../config/env.js";
|
|
8
|
-
import { readConfig, resolveConfigPath } from "../config/store.js";
|
|
9
|
-
|
|
10
|
-
function hashToken(token: string) {
|
|
11
|
-
return createHash("sha256").update(token).digest("hex");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function createInviteToken() {
|
|
15
|
-
return `pcp_bootstrap_${randomBytes(24).toString("hex")}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function resolveDbUrl(configPath?: string, explicitDbUrl?: string) {
|
|
19
|
-
if (explicitDbUrl) return explicitDbUrl;
|
|
20
|
-
const config = readConfig(configPath);
|
|
21
|
-
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
|
|
22
|
-
if (config?.database.mode === "postgres" && config.database.connectionString) {
|
|
23
|
-
return config.database.connectionString;
|
|
24
|
-
}
|
|
25
|
-
if (config?.database.mode === "embedded-postgres") {
|
|
26
|
-
const port = config.database.embeddedPostgresPort ?? 54329;
|
|
27
|
-
return `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function resolveBaseUrl(configPath?: string, explicitBaseUrl?: string) {
|
|
33
|
-
if (explicitBaseUrl) return explicitBaseUrl.replace(/\/+$/, "");
|
|
34
|
-
const fromEnv =
|
|
35
|
-
process.env.PAPERCLIP_PUBLIC_URL ??
|
|
36
|
-
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
|
|
37
|
-
process.env.BETTER_AUTH_URL ??
|
|
38
|
-
process.env.BETTER_AUTH_BASE_URL;
|
|
39
|
-
if (fromEnv?.trim()) return fromEnv.trim().replace(/\/+$/, "");
|
|
40
|
-
const config = readConfig(configPath);
|
|
41
|
-
if (config?.auth.baseUrlMode === "explicit" && config.auth.publicBaseUrl) {
|
|
42
|
-
return config.auth.publicBaseUrl.replace(/\/+$/, "");
|
|
43
|
-
}
|
|
44
|
-
const bind = config?.server.bind ?? inferBindModeFromHost(config?.server.host);
|
|
45
|
-
const host =
|
|
46
|
-
bind === "custom"
|
|
47
|
-
? config?.server.customBindHost ?? config?.server.host ?? "localhost"
|
|
48
|
-
: config?.server.host ?? "localhost";
|
|
49
|
-
const port = config?.server.port ?? 3100;
|
|
50
|
-
const publicHost = host === "0.0.0.0" || bind === "lan" ? "localhost" : host;
|
|
51
|
-
return `http://${publicHost}:${port}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function bootstrapCeoInvite(opts: {
|
|
55
|
-
config?: string;
|
|
56
|
-
force?: boolean;
|
|
57
|
-
expiresHours?: number;
|
|
58
|
-
baseUrl?: string;
|
|
59
|
-
dbUrl?: string;
|
|
60
|
-
}) {
|
|
61
|
-
const configPath = resolveConfigPath(opts.config);
|
|
62
|
-
loadPaperclipEnvFile(configPath);
|
|
63
|
-
const config = readConfig(configPath);
|
|
64
|
-
if (!config) {
|
|
65
|
-
p.log.error(`No config found at ${configPath}. Run ${pc.cyan("paperclip onboard")} first.`);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (config.server.deploymentMode !== "authenticated") {
|
|
70
|
-
p.log.info("Deployment mode is local_trusted. Bootstrap CEO invite is only required for authenticated mode.");
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const dbUrl = resolveDbUrl(configPath, opts.dbUrl);
|
|
75
|
-
if (!dbUrl) {
|
|
76
|
-
p.log.error(
|
|
77
|
-
"Could not resolve database connection for bootstrap.",
|
|
78
|
-
);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const db = createDb(dbUrl);
|
|
83
|
-
const closableDb = db as typeof db & {
|
|
84
|
-
$client?: {
|
|
85
|
-
end?: (options?: { timeout?: number }) => Promise<void>;
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
try {
|
|
89
|
-
const existingAdminCount = await db
|
|
90
|
-
.select()
|
|
91
|
-
.from(instanceUserRoles)
|
|
92
|
-
.where(eq(instanceUserRoles.role, "instance_admin"))
|
|
93
|
-
.then((rows) => rows.length);
|
|
94
|
-
|
|
95
|
-
if (existingAdminCount > 0 && !opts.force) {
|
|
96
|
-
p.log.info("Instance already has an admin user. Use --force to generate a new bootstrap invite.");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const now = new Date();
|
|
101
|
-
await db
|
|
102
|
-
.update(invites)
|
|
103
|
-
.set({ revokedAt: now, updatedAt: now })
|
|
104
|
-
.where(
|
|
105
|
-
and(
|
|
106
|
-
eq(invites.inviteType, "bootstrap_ceo"),
|
|
107
|
-
isNull(invites.revokedAt),
|
|
108
|
-
isNull(invites.acceptedAt),
|
|
109
|
-
gt(invites.expiresAt, now),
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const token = createInviteToken();
|
|
114
|
-
const expiresHours = Math.max(1, Math.min(24 * 30, opts.expiresHours ?? 72));
|
|
115
|
-
const created = await db
|
|
116
|
-
.insert(invites)
|
|
117
|
-
.values({
|
|
118
|
-
inviteType: "bootstrap_ceo",
|
|
119
|
-
tokenHash: hashToken(token),
|
|
120
|
-
allowedJoinTypes: "human",
|
|
121
|
-
expiresAt: new Date(Date.now() + expiresHours * 60 * 60 * 1000),
|
|
122
|
-
invitedByUserId: "system",
|
|
123
|
-
})
|
|
124
|
-
.returning()
|
|
125
|
-
.then((rows) => rows[0]);
|
|
126
|
-
|
|
127
|
-
const baseUrl = resolveBaseUrl(configPath, opts.baseUrl);
|
|
128
|
-
const inviteUrl = `${baseUrl}/invite/${token}`;
|
|
129
|
-
p.log.success("Created bootstrap CEO invite.");
|
|
130
|
-
p.log.message(`Invite URL: ${pc.cyan(inviteUrl)}`);
|
|
131
|
-
p.log.message(`Expires: ${pc.dim(created.expiresAt.toISOString())}`);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
p.log.error(`Could not create bootstrap invite: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
-
p.log.info("If using embedded-postgres, start the Paperclip server and run this command again.");
|
|
135
|
-
} finally {
|
|
136
|
-
await closableDb.$client?.end?.({ timeout: 5 }).catch(() => undefined);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import type { ActivityEvent } from "@paperclipai/shared";
|
|
3
|
-
import {
|
|
4
|
-
addCommonClientOptions,
|
|
5
|
-
formatInlineRecord,
|
|
6
|
-
handleCommandError,
|
|
7
|
-
printOutput,
|
|
8
|
-
resolveCommandContext,
|
|
9
|
-
type BaseClientOptions,
|
|
10
|
-
} from "./common.js";
|
|
11
|
-
|
|
12
|
-
interface ActivityListOptions extends BaseClientOptions {
|
|
13
|
-
companyId?: string;
|
|
14
|
-
agentId?: string;
|
|
15
|
-
entityType?: string;
|
|
16
|
-
entityId?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function registerActivityCommands(program: Command): void {
|
|
20
|
-
const activity = program.command("activity").description("Activity log operations");
|
|
21
|
-
|
|
22
|
-
addCommonClientOptions(
|
|
23
|
-
activity
|
|
24
|
-
.command("list")
|
|
25
|
-
.description("List company activity log entries")
|
|
26
|
-
.requiredOption("-C, --company-id <id>", "Company ID")
|
|
27
|
-
.option("--agent-id <id>", "Filter by agent ID")
|
|
28
|
-
.option("--entity-type <type>", "Filter by entity type")
|
|
29
|
-
.option("--entity-id <id>", "Filter by entity ID")
|
|
30
|
-
.action(async (opts: ActivityListOptions) => {
|
|
31
|
-
try {
|
|
32
|
-
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
|
33
|
-
const params = new URLSearchParams();
|
|
34
|
-
if (opts.agentId) params.set("agentId", opts.agentId);
|
|
35
|
-
if (opts.entityType) params.set("entityType", opts.entityType);
|
|
36
|
-
if (opts.entityId) params.set("entityId", opts.entityId);
|
|
37
|
-
|
|
38
|
-
const query = params.toString();
|
|
39
|
-
const path = `/api/companies/${ctx.companyId}/activity${query ? `?${query}` : ""}`;
|
|
40
|
-
const rows = (await ctx.api.get<ActivityEvent[]>(path)) ?? [];
|
|
41
|
-
|
|
42
|
-
if (ctx.json) {
|
|
43
|
-
printOutput(rows, { json: true });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (rows.length === 0) {
|
|
48
|
-
printOutput([], { json: false });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const row of rows) {
|
|
53
|
-
console.log(
|
|
54
|
-
formatInlineRecord({
|
|
55
|
-
id: row.id,
|
|
56
|
-
action: row.action,
|
|
57
|
-
actorType: row.actorType,
|
|
58
|
-
actorId: row.actorId,
|
|
59
|
-
entityType: row.entityType,
|
|
60
|
-
entityId: row.entityId,
|
|
61
|
-
createdAt: String(row.createdAt),
|
|
62
|
-
}),
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
} catch (err) {
|
|
66
|
-
handleCommandError(err);
|
|
67
|
-
}
|
|
68
|
-
}),
|
|
69
|
-
{ includeCompany: false },
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import type { Agent } from "@paperclipai/shared";
|
|
3
|
-
import {
|
|
4
|
-
removeMaintainerOnlySkillSymlinks,
|
|
5
|
-
resolvePaperclipSkillsDir,
|
|
6
|
-
} from "@paperclipai/adapter-utils/server-utils";
|
|
7
|
-
import fs from "node:fs/promises";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
12
|
-
addCommonClientOptions,
|
|
13
|
-
formatInlineRecord,
|
|
14
|
-
handleCommandError,
|
|
15
|
-
printOutput,
|
|
16
|
-
resolveCommandContext,
|
|
17
|
-
type BaseClientOptions,
|
|
18
|
-
} from "./common.js";
|
|
19
|
-
|
|
20
|
-
interface AgentListOptions extends BaseClientOptions {
|
|
21
|
-
companyId?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface AgentLocalCliOptions extends BaseClientOptions {
|
|
25
|
-
companyId?: string;
|
|
26
|
-
keyName?: string;
|
|
27
|
-
installSkills?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface CreatedAgentKey {
|
|
31
|
-
id: string;
|
|
32
|
-
name: string;
|
|
33
|
-
token: string;
|
|
34
|
-
createdAt: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface SkillsInstallSummary {
|
|
38
|
-
tool: "codex" | "claude";
|
|
39
|
-
target: string;
|
|
40
|
-
linked: string[];
|
|
41
|
-
removed: string[];
|
|
42
|
-
skipped: string[];
|
|
43
|
-
failed: Array<{ name: string; error: string }>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
-
|
|
48
|
-
function codexSkillsHome(): string {
|
|
49
|
-
const fromEnv = process.env.CODEX_HOME?.trim();
|
|
50
|
-
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".codex");
|
|
51
|
-
return path.join(base, "skills");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function claudeSkillsHome(): string {
|
|
55
|
-
const fromEnv = process.env.CLAUDE_HOME?.trim();
|
|
56
|
-
const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".claude");
|
|
57
|
-
return path.join(base, "skills");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function installSkillsForTarget(
|
|
61
|
-
sourceSkillsDir: string,
|
|
62
|
-
targetSkillsDir: string,
|
|
63
|
-
tool: "codex" | "claude",
|
|
64
|
-
): Promise<SkillsInstallSummary> {
|
|
65
|
-
const summary: SkillsInstallSummary = {
|
|
66
|
-
tool,
|
|
67
|
-
target: targetSkillsDir,
|
|
68
|
-
linked: [],
|
|
69
|
-
removed: [],
|
|
70
|
-
skipped: [],
|
|
71
|
-
failed: [],
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
await fs.mkdir(targetSkillsDir, { recursive: true });
|
|
75
|
-
const entries = await fs.readdir(sourceSkillsDir, { withFileTypes: true });
|
|
76
|
-
summary.removed = await removeMaintainerOnlySkillSymlinks(
|
|
77
|
-
targetSkillsDir,
|
|
78
|
-
entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
|
|
79
|
-
);
|
|
80
|
-
for (const entry of entries) {
|
|
81
|
-
if (!entry.isDirectory()) continue;
|
|
82
|
-
const source = path.join(sourceSkillsDir, entry.name);
|
|
83
|
-
const target = path.join(targetSkillsDir, entry.name);
|
|
84
|
-
const existing = await fs.lstat(target).catch(() => null);
|
|
85
|
-
if (existing) {
|
|
86
|
-
if (existing.isSymbolicLink()) {
|
|
87
|
-
let linkedPath: string | null = null;
|
|
88
|
-
try {
|
|
89
|
-
linkedPath = await fs.readlink(target);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
await fs.unlink(target);
|
|
92
|
-
try {
|
|
93
|
-
await fs.symlink(source, target);
|
|
94
|
-
summary.linked.push(entry.name);
|
|
95
|
-
continue;
|
|
96
|
-
} catch (linkErr) {
|
|
97
|
-
summary.failed.push({
|
|
98
|
-
name: entry.name,
|
|
99
|
-
error:
|
|
100
|
-
err instanceof Error && linkErr instanceof Error
|
|
101
|
-
? `${err.message}; then ${linkErr.message}`
|
|
102
|
-
: err instanceof Error
|
|
103
|
-
? err.message
|
|
104
|
-
: `Failed to recover broken symlink: ${String(err)}`,
|
|
105
|
-
});
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const resolvedLinkedPath = path.isAbsolute(linkedPath)
|
|
111
|
-
? linkedPath
|
|
112
|
-
: path.resolve(path.dirname(target), linkedPath);
|
|
113
|
-
const linkedTargetExists = await fs
|
|
114
|
-
.stat(resolvedLinkedPath)
|
|
115
|
-
.then(() => true)
|
|
116
|
-
.catch(() => false);
|
|
117
|
-
|
|
118
|
-
if (!linkedTargetExists) {
|
|
119
|
-
await fs.unlink(target);
|
|
120
|
-
} else {
|
|
121
|
-
summary.skipped.push(entry.name);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
summary.skipped.push(entry.name);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
await fs.symlink(source, target);
|
|
132
|
-
summary.linked.push(entry.name);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
summary.failed.push({
|
|
135
|
-
name: entry.name,
|
|
136
|
-
error: err instanceof Error ? err.message : String(err),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return summary;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function buildAgentEnvExports(input: {
|
|
145
|
-
apiBase: string;
|
|
146
|
-
companyId: string;
|
|
147
|
-
agentId: string;
|
|
148
|
-
apiKey: string;
|
|
149
|
-
}): string {
|
|
150
|
-
const escaped = (value: string) => value.replace(/'/g, "'\"'\"'");
|
|
151
|
-
return [
|
|
152
|
-
`export PAPERCLIP_API_URL='${escaped(input.apiBase)}'`,
|
|
153
|
-
`export PAPERCLIP_COMPANY_ID='${escaped(input.companyId)}'`,
|
|
154
|
-
`export PAPERCLIP_AGENT_ID='${escaped(input.agentId)}'`,
|
|
155
|
-
`export PAPERCLIP_API_KEY='${escaped(input.apiKey)}'`,
|
|
156
|
-
].join("\n");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function registerAgentCommands(program: Command): void {
|
|
160
|
-
const agent = program.command("agent").description("Agent operations");
|
|
161
|
-
|
|
162
|
-
addCommonClientOptions(
|
|
163
|
-
agent
|
|
164
|
-
.command("list")
|
|
165
|
-
.description("List agents for a company")
|
|
166
|
-
.requiredOption("-C, --company-id <id>", "Company ID")
|
|
167
|
-
.action(async (opts: AgentListOptions) => {
|
|
168
|
-
try {
|
|
169
|
-
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
|
170
|
-
const rows = (await ctx.api.get<Agent[]>(`/api/companies/${ctx.companyId}/agents`)) ?? [];
|
|
171
|
-
|
|
172
|
-
if (ctx.json) {
|
|
173
|
-
printOutput(rows, { json: true });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (rows.length === 0) {
|
|
178
|
-
printOutput([], { json: false });
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const row of rows) {
|
|
183
|
-
console.log(
|
|
184
|
-
formatInlineRecord({
|
|
185
|
-
id: row.id,
|
|
186
|
-
name: row.name,
|
|
187
|
-
role: row.role,
|
|
188
|
-
status: row.status,
|
|
189
|
-
reportsTo: row.reportsTo,
|
|
190
|
-
budgetMonthlyCents: row.budgetMonthlyCents,
|
|
191
|
-
spentMonthlyCents: row.spentMonthlyCents,
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
handleCommandError(err);
|
|
197
|
-
}
|
|
198
|
-
}),
|
|
199
|
-
{ includeCompany: false },
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
addCommonClientOptions(
|
|
203
|
-
agent
|
|
204
|
-
.command("get")
|
|
205
|
-
.description("Get one agent")
|
|
206
|
-
.argument("<agentId>", "Agent ID")
|
|
207
|
-
.action(async (agentId: string, opts: BaseClientOptions) => {
|
|
208
|
-
try {
|
|
209
|
-
const ctx = resolveCommandContext(opts);
|
|
210
|
-
const row = await ctx.api.get<Agent>(`/api/agents/${agentId}`);
|
|
211
|
-
printOutput(row, { json: ctx.json });
|
|
212
|
-
} catch (err) {
|
|
213
|
-
handleCommandError(err);
|
|
214
|
-
}
|
|
215
|
-
}),
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
addCommonClientOptions(
|
|
219
|
-
agent
|
|
220
|
-
.command("local-cli")
|
|
221
|
-
.description(
|
|
222
|
-
"Create an agent API key, install local Paperclip skills for Codex/Claude, and print shell exports",
|
|
223
|
-
)
|
|
224
|
-
.argument("<agentRef>", "Agent ID or shortname/url-key")
|
|
225
|
-
.requiredOption("-C, --company-id <id>", "Company ID")
|
|
226
|
-
.option("--key-name <name>", "API key label", "local-cli")
|
|
227
|
-
.option(
|
|
228
|
-
"--no-install-skills",
|
|
229
|
-
"Skip installing Paperclip skills into ~/.codex/skills and ~/.claude/skills",
|
|
230
|
-
)
|
|
231
|
-
.action(async (agentRef: string, opts: AgentLocalCliOptions) => {
|
|
232
|
-
try {
|
|
233
|
-
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
|
234
|
-
const query = new URLSearchParams({ companyId: ctx.companyId ?? "" });
|
|
235
|
-
const agentRow = await ctx.api.get<Agent>(
|
|
236
|
-
`/api/agents/${encodeURIComponent(agentRef)}?${query.toString()}`,
|
|
237
|
-
);
|
|
238
|
-
if (!agentRow) {
|
|
239
|
-
throw new Error(`Agent not found: ${agentRef}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const now = new Date().toISOString().replaceAll(":", "-");
|
|
243
|
-
const keyName = opts.keyName?.trim() ? opts.keyName.trim() : `local-cli-${now}`;
|
|
244
|
-
const key = await ctx.api.post<CreatedAgentKey>(`/api/agents/${agentRow.id}/keys`, { name: keyName });
|
|
245
|
-
if (!key) {
|
|
246
|
-
throw new Error("Failed to create API key");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const installSummaries: SkillsInstallSummary[] = [];
|
|
250
|
-
if (opts.installSkills !== false) {
|
|
251
|
-
const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [path.resolve(process.cwd(), "skills")]);
|
|
252
|
-
if (!skillsDir) {
|
|
253
|
-
throw new Error(
|
|
254
|
-
"Could not locate local Paperclip skills directory. Expected ./skills in the repo checkout.",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
installSummaries.push(
|
|
259
|
-
await installSkillsForTarget(skillsDir, codexSkillsHome(), "codex"),
|
|
260
|
-
await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"),
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const exportsText = buildAgentEnvExports({
|
|
265
|
-
apiBase: ctx.api.apiBase,
|
|
266
|
-
companyId: agentRow.companyId,
|
|
267
|
-
agentId: agentRow.id,
|
|
268
|
-
apiKey: key.token,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
if (ctx.json) {
|
|
272
|
-
printOutput(
|
|
273
|
-
{
|
|
274
|
-
agent: {
|
|
275
|
-
id: agentRow.id,
|
|
276
|
-
name: agentRow.name,
|
|
277
|
-
urlKey: agentRow.urlKey,
|
|
278
|
-
companyId: agentRow.companyId,
|
|
279
|
-
},
|
|
280
|
-
key: {
|
|
281
|
-
id: key.id,
|
|
282
|
-
name: key.name,
|
|
283
|
-
createdAt: key.createdAt,
|
|
284
|
-
token: key.token,
|
|
285
|
-
},
|
|
286
|
-
skills: installSummaries,
|
|
287
|
-
exports: exportsText,
|
|
288
|
-
},
|
|
289
|
-
{ json: true },
|
|
290
|
-
);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
console.log(`Agent: ${agentRow.name} (${agentRow.id})`);
|
|
295
|
-
console.log(`API key created: ${key.name} (${key.id})`);
|
|
296
|
-
if (installSummaries.length > 0) {
|
|
297
|
-
for (const summary of installSummaries) {
|
|
298
|
-
console.log(
|
|
299
|
-
`${summary.tool}: linked=${summary.linked.length} removed=${summary.removed.length} skipped=${summary.skipped.length} failed=${summary.failed.length} target=${summary.target}`,
|
|
300
|
-
);
|
|
301
|
-
for (const failed of summary.failed) {
|
|
302
|
-
console.log(` failed ${failed.name}: ${failed.error}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
console.log("");
|
|
307
|
-
console.log("# Run this in your shell before launching codex/claude:");
|
|
308
|
-
console.log(exportsText);
|
|
309
|
-
} catch (err) {
|
|
310
|
-
handleCommandError(err);
|
|
311
|
-
}
|
|
312
|
-
}),
|
|
313
|
-
{ includeCompany: false },
|
|
314
|
-
);
|
|
315
|
-
}
|