offwatch 0.5.11 → 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.
Files changed (95) hide show
  1. package/README.md +132 -178
  2. package/bin/offwatch.js +6 -7
  3. package/lib/downloader.js +112 -0
  4. package/package.json +17 -7
  5. package/postinstall.js +21 -0
  6. package/src/__tests__/agent-jwt-env.test.ts +0 -79
  7. package/src/__tests__/allowed-hostname.test.ts +0 -80
  8. package/src/__tests__/auth-command-registration.test.ts +0 -16
  9. package/src/__tests__/board-auth.test.ts +0 -53
  10. package/src/__tests__/common.test.ts +0 -98
  11. package/src/__tests__/company-delete.test.ts +0 -95
  12. package/src/__tests__/company-import-export-e2e.test.ts +0 -502
  13. package/src/__tests__/company-import-url.test.ts +0 -74
  14. package/src/__tests__/company-import-zip.test.ts +0 -44
  15. package/src/__tests__/company.test.ts +0 -599
  16. package/src/__tests__/context.test.ts +0 -70
  17. package/src/__tests__/data-dir.test.ts +0 -79
  18. package/src/__tests__/doctor.test.ts +0 -102
  19. package/src/__tests__/feedback.test.ts +0 -177
  20. package/src/__tests__/helpers/embedded-postgres.ts +0 -6
  21. package/src/__tests__/helpers/zip.ts +0 -87
  22. package/src/__tests__/home-paths.test.ts +0 -44
  23. package/src/__tests__/http.test.ts +0 -106
  24. package/src/__tests__/network-bind.test.ts +0 -62
  25. package/src/__tests__/onboard.test.ts +0 -166
  26. package/src/__tests__/routines.test.ts +0 -249
  27. package/src/__tests__/telemetry.test.ts +0 -117
  28. package/src/__tests__/worktree-merge-history.test.ts +0 -492
  29. package/src/__tests__/worktree.test.ts +0 -982
  30. package/src/adapters/http/format-event.ts +0 -4
  31. package/src/adapters/http/index.ts +0 -7
  32. package/src/adapters/index.ts +0 -2
  33. package/src/adapters/process/format-event.ts +0 -4
  34. package/src/adapters/process/index.ts +0 -7
  35. package/src/adapters/registry.ts +0 -63
  36. package/src/checks/agent-jwt-secret-check.ts +0 -40
  37. package/src/checks/config-check.ts +0 -33
  38. package/src/checks/database-check.ts +0 -59
  39. package/src/checks/deployment-auth-check.ts +0 -88
  40. package/src/checks/index.ts +0 -18
  41. package/src/checks/llm-check.ts +0 -82
  42. package/src/checks/log-check.ts +0 -30
  43. package/src/checks/path-resolver.ts +0 -1
  44. package/src/checks/port-check.ts +0 -24
  45. package/src/checks/secrets-check.ts +0 -146
  46. package/src/checks/storage-check.ts +0 -51
  47. package/src/client/board-auth.ts +0 -282
  48. package/src/client/command-label.ts +0 -4
  49. package/src/client/context.ts +0 -175
  50. package/src/client/http.ts +0 -255
  51. package/src/commands/allowed-hostname.ts +0 -40
  52. package/src/commands/auth-bootstrap-ceo.ts +0 -138
  53. package/src/commands/client/activity.ts +0 -71
  54. package/src/commands/client/agent.ts +0 -315
  55. package/src/commands/client/approval.ts +0 -259
  56. package/src/commands/client/auth.ts +0 -113
  57. package/src/commands/client/common.ts +0 -221
  58. package/src/commands/client/company.ts +0 -1578
  59. package/src/commands/client/context.ts +0 -125
  60. package/src/commands/client/dashboard.ts +0 -34
  61. package/src/commands/client/feedback.ts +0 -645
  62. package/src/commands/client/issue.ts +0 -411
  63. package/src/commands/client/plugin.ts +0 -374
  64. package/src/commands/client/zip.ts +0 -129
  65. package/src/commands/configure.ts +0 -201
  66. package/src/commands/db-backup.ts +0 -102
  67. package/src/commands/doctor.ts +0 -203
  68. package/src/commands/env.ts +0 -411
  69. package/src/commands/heartbeat-run.ts +0 -344
  70. package/src/commands/onboard.ts +0 -692
  71. package/src/commands/routines.ts +0 -352
  72. package/src/commands/run.ts +0 -216
  73. package/src/commands/worktree-lib.ts +0 -279
  74. package/src/commands/worktree-merge-history-lib.ts +0 -764
  75. package/src/commands/worktree.ts +0 -2876
  76. package/src/config/data-dir.ts +0 -48
  77. package/src/config/env.ts +0 -125
  78. package/src/config/home.ts +0 -80
  79. package/src/config/hostnames.ts +0 -26
  80. package/src/config/schema.ts +0 -30
  81. package/src/config/secrets-key.ts +0 -48
  82. package/src/config/server-bind.ts +0 -183
  83. package/src/config/store.ts +0 -120
  84. package/src/index.ts +0 -182
  85. package/src/prompts/database.ts +0 -157
  86. package/src/prompts/llm.ts +0 -43
  87. package/src/prompts/logging.ts +0 -37
  88. package/src/prompts/secrets.ts +0 -99
  89. package/src/prompts/server.ts +0 -221
  90. package/src/prompts/storage.ts +0 -146
  91. package/src/telemetry.ts +0 -49
  92. package/src/utils/banner.ts +0 -24
  93. package/src/utils/net.ts +0 -18
  94. package/src/utils/path-resolver.ts +0 -25
  95. 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
- }