offwatch 0.5.12 → 0.5.14
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 +18 -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,259 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
createApprovalSchema,
|
|
4
|
-
requestApprovalRevisionSchema,
|
|
5
|
-
resolveApprovalSchema,
|
|
6
|
-
resubmitApprovalSchema,
|
|
7
|
-
type Approval,
|
|
8
|
-
type ApprovalComment,
|
|
9
|
-
} from "@paperclipai/shared";
|
|
10
|
-
import {
|
|
11
|
-
addCommonClientOptions,
|
|
12
|
-
formatInlineRecord,
|
|
13
|
-
handleCommandError,
|
|
14
|
-
printOutput,
|
|
15
|
-
resolveCommandContext,
|
|
16
|
-
type BaseClientOptions,
|
|
17
|
-
} from "./common.js";
|
|
18
|
-
|
|
19
|
-
interface ApprovalListOptions extends BaseClientOptions {
|
|
20
|
-
companyId?: string;
|
|
21
|
-
status?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ApprovalDecisionOptions extends BaseClientOptions {
|
|
25
|
-
decisionNote?: string;
|
|
26
|
-
decidedByUserId?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ApprovalCreateOptions extends BaseClientOptions {
|
|
30
|
-
companyId?: string;
|
|
31
|
-
type: string;
|
|
32
|
-
requestedByAgentId?: string;
|
|
33
|
-
payload: string;
|
|
34
|
-
issueIds?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ApprovalResubmitOptions extends BaseClientOptions {
|
|
38
|
-
payload?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface ApprovalCommentOptions extends BaseClientOptions {
|
|
42
|
-
body: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function registerApprovalCommands(program: Command): void {
|
|
46
|
-
const approval = program.command("approval").description("Approval operations");
|
|
47
|
-
|
|
48
|
-
addCommonClientOptions(
|
|
49
|
-
approval
|
|
50
|
-
.command("list")
|
|
51
|
-
.description("List approvals for a company")
|
|
52
|
-
.requiredOption("-C, --company-id <id>", "Company ID")
|
|
53
|
-
.option("--status <status>", "Status filter")
|
|
54
|
-
.action(async (opts: ApprovalListOptions) => {
|
|
55
|
-
try {
|
|
56
|
-
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
|
57
|
-
const params = new URLSearchParams();
|
|
58
|
-
if (opts.status) params.set("status", opts.status);
|
|
59
|
-
const query = params.toString();
|
|
60
|
-
const rows =
|
|
61
|
-
(await ctx.api.get<Approval[]>(
|
|
62
|
-
`/api/companies/${ctx.companyId}/approvals${query ? `?${query}` : ""}`,
|
|
63
|
-
)) ?? [];
|
|
64
|
-
|
|
65
|
-
if (ctx.json) {
|
|
66
|
-
printOutput(rows, { json: true });
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (rows.length === 0) {
|
|
71
|
-
printOutput([], { json: false });
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const row of rows) {
|
|
76
|
-
console.log(
|
|
77
|
-
formatInlineRecord({
|
|
78
|
-
id: row.id,
|
|
79
|
-
type: row.type,
|
|
80
|
-
status: row.status,
|
|
81
|
-
requestedByAgentId: row.requestedByAgentId,
|
|
82
|
-
requestedByUserId: row.requestedByUserId,
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
} catch (err) {
|
|
87
|
-
handleCommandError(err);
|
|
88
|
-
}
|
|
89
|
-
}),
|
|
90
|
-
{ includeCompany: false },
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
addCommonClientOptions(
|
|
94
|
-
approval
|
|
95
|
-
.command("get")
|
|
96
|
-
.description("Get one approval")
|
|
97
|
-
.argument("<approvalId>", "Approval ID")
|
|
98
|
-
.action(async (approvalId: string, opts: BaseClientOptions) => {
|
|
99
|
-
try {
|
|
100
|
-
const ctx = resolveCommandContext(opts);
|
|
101
|
-
const row = await ctx.api.get<Approval>(`/api/approvals/${approvalId}`);
|
|
102
|
-
printOutput(row, { json: ctx.json });
|
|
103
|
-
} catch (err) {
|
|
104
|
-
handleCommandError(err);
|
|
105
|
-
}
|
|
106
|
-
}),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
addCommonClientOptions(
|
|
110
|
-
approval
|
|
111
|
-
.command("create")
|
|
112
|
-
.description("Create an approval request")
|
|
113
|
-
.requiredOption("-C, --company-id <id>", "Company ID")
|
|
114
|
-
.requiredOption("--type <type>", "Approval type (hire_agent|approve_ceo_strategy)")
|
|
115
|
-
.requiredOption("--payload <json>", "Approval payload as JSON object")
|
|
116
|
-
.option("--requested-by-agent-id <id>", "Requesting agent ID")
|
|
117
|
-
.option("--issue-ids <csv>", "Comma-separated linked issue IDs")
|
|
118
|
-
.action(async (opts: ApprovalCreateOptions) => {
|
|
119
|
-
try {
|
|
120
|
-
const ctx = resolveCommandContext(opts, { requireCompany: true });
|
|
121
|
-
const payloadJson = parseJsonObject(opts.payload, "payload");
|
|
122
|
-
const payload = createApprovalSchema.parse({
|
|
123
|
-
type: opts.type,
|
|
124
|
-
payload: payloadJson,
|
|
125
|
-
requestedByAgentId: opts.requestedByAgentId,
|
|
126
|
-
issueIds: parseCsv(opts.issueIds),
|
|
127
|
-
});
|
|
128
|
-
const created = await ctx.api.post<Approval>(`/api/companies/${ctx.companyId}/approvals`, payload);
|
|
129
|
-
printOutput(created, { json: ctx.json });
|
|
130
|
-
} catch (err) {
|
|
131
|
-
handleCommandError(err);
|
|
132
|
-
}
|
|
133
|
-
}),
|
|
134
|
-
{ includeCompany: false },
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
addCommonClientOptions(
|
|
138
|
-
approval
|
|
139
|
-
.command("approve")
|
|
140
|
-
.description("Approve an approval request")
|
|
141
|
-
.argument("<approvalId>", "Approval ID")
|
|
142
|
-
.option("--decision-note <text>", "Decision note")
|
|
143
|
-
.option("--decided-by-user-id <id>", "Decision actor user ID")
|
|
144
|
-
.action(async (approvalId: string, opts: ApprovalDecisionOptions) => {
|
|
145
|
-
try {
|
|
146
|
-
const ctx = resolveCommandContext(opts);
|
|
147
|
-
const payload = resolveApprovalSchema.parse({
|
|
148
|
-
decisionNote: opts.decisionNote,
|
|
149
|
-
decidedByUserId: opts.decidedByUserId,
|
|
150
|
-
});
|
|
151
|
-
const updated = await ctx.api.post<Approval>(`/api/approvals/${approvalId}/approve`, payload);
|
|
152
|
-
printOutput(updated, { json: ctx.json });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
handleCommandError(err);
|
|
155
|
-
}
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
addCommonClientOptions(
|
|
160
|
-
approval
|
|
161
|
-
.command("reject")
|
|
162
|
-
.description("Reject an approval request")
|
|
163
|
-
.argument("<approvalId>", "Approval ID")
|
|
164
|
-
.option("--decision-note <text>", "Decision note")
|
|
165
|
-
.option("--decided-by-user-id <id>", "Decision actor user ID")
|
|
166
|
-
.action(async (approvalId: string, opts: ApprovalDecisionOptions) => {
|
|
167
|
-
try {
|
|
168
|
-
const ctx = resolveCommandContext(opts);
|
|
169
|
-
const payload = resolveApprovalSchema.parse({
|
|
170
|
-
decisionNote: opts.decisionNote,
|
|
171
|
-
decidedByUserId: opts.decidedByUserId,
|
|
172
|
-
});
|
|
173
|
-
const updated = await ctx.api.post<Approval>(`/api/approvals/${approvalId}/reject`, payload);
|
|
174
|
-
printOutput(updated, { json: ctx.json });
|
|
175
|
-
} catch (err) {
|
|
176
|
-
handleCommandError(err);
|
|
177
|
-
}
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
addCommonClientOptions(
|
|
182
|
-
approval
|
|
183
|
-
.command("request-revision")
|
|
184
|
-
.description("Request revision for an approval")
|
|
185
|
-
.argument("<approvalId>", "Approval ID")
|
|
186
|
-
.option("--decision-note <text>", "Decision note")
|
|
187
|
-
.option("--decided-by-user-id <id>", "Decision actor user ID")
|
|
188
|
-
.action(async (approvalId: string, opts: ApprovalDecisionOptions) => {
|
|
189
|
-
try {
|
|
190
|
-
const ctx = resolveCommandContext(opts);
|
|
191
|
-
const payload = requestApprovalRevisionSchema.parse({
|
|
192
|
-
decisionNote: opts.decisionNote,
|
|
193
|
-
decidedByUserId: opts.decidedByUserId,
|
|
194
|
-
});
|
|
195
|
-
const updated = await ctx.api.post<Approval>(`/api/approvals/${approvalId}/request-revision`, payload);
|
|
196
|
-
printOutput(updated, { json: ctx.json });
|
|
197
|
-
} catch (err) {
|
|
198
|
-
handleCommandError(err);
|
|
199
|
-
}
|
|
200
|
-
}),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
addCommonClientOptions(
|
|
204
|
-
approval
|
|
205
|
-
.command("resubmit")
|
|
206
|
-
.description("Resubmit an approval (optionally with new payload)")
|
|
207
|
-
.argument("<approvalId>", "Approval ID")
|
|
208
|
-
.option("--payload <json>", "Payload JSON object")
|
|
209
|
-
.action(async (approvalId: string, opts: ApprovalResubmitOptions) => {
|
|
210
|
-
try {
|
|
211
|
-
const ctx = resolveCommandContext(opts);
|
|
212
|
-
const payload = resubmitApprovalSchema.parse({
|
|
213
|
-
payload: opts.payload ? parseJsonObject(opts.payload, "payload") : undefined,
|
|
214
|
-
});
|
|
215
|
-
const updated = await ctx.api.post<Approval>(`/api/approvals/${approvalId}/resubmit`, payload);
|
|
216
|
-
printOutput(updated, { json: ctx.json });
|
|
217
|
-
} catch (err) {
|
|
218
|
-
handleCommandError(err);
|
|
219
|
-
}
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
addCommonClientOptions(
|
|
224
|
-
approval
|
|
225
|
-
.command("comment")
|
|
226
|
-
.description("Add comment to an approval")
|
|
227
|
-
.argument("<approvalId>", "Approval ID")
|
|
228
|
-
.requiredOption("--body <text>", "Comment body")
|
|
229
|
-
.action(async (approvalId: string, opts: ApprovalCommentOptions) => {
|
|
230
|
-
try {
|
|
231
|
-
const ctx = resolveCommandContext(opts);
|
|
232
|
-
const created = await ctx.api.post<ApprovalComment>(`/api/approvals/${approvalId}/comments`, {
|
|
233
|
-
body: opts.body,
|
|
234
|
-
});
|
|
235
|
-
printOutput(created, { json: ctx.json });
|
|
236
|
-
} catch (err) {
|
|
237
|
-
handleCommandError(err);
|
|
238
|
-
}
|
|
239
|
-
}),
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function parseCsv(value: string | undefined): string[] | undefined {
|
|
244
|
-
if (!value) return undefined;
|
|
245
|
-
const rows = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
246
|
-
return rows.length > 0 ? rows : undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function parseJsonObject(value: string, name: string): Record<string, unknown> {
|
|
250
|
-
try {
|
|
251
|
-
const parsed = JSON.parse(value) as unknown;
|
|
252
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
253
|
-
throw new Error(`${name} must be a JSON object`);
|
|
254
|
-
}
|
|
255
|
-
return parsed as Record<string, unknown>;
|
|
256
|
-
} catch (err) {
|
|
257
|
-
throw new Error(`Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
getStoredBoardCredential,
|
|
4
|
-
loginBoardCli,
|
|
5
|
-
removeStoredBoardCredential,
|
|
6
|
-
revokeStoredBoardCredential,
|
|
7
|
-
} from "../../client/board-auth.js";
|
|
8
|
-
import {
|
|
9
|
-
addCommonClientOptions,
|
|
10
|
-
handleCommandError,
|
|
11
|
-
printOutput,
|
|
12
|
-
resolveCommandContext,
|
|
13
|
-
type BaseClientOptions,
|
|
14
|
-
} from "./common.js";
|
|
15
|
-
|
|
16
|
-
interface AuthLoginOptions extends BaseClientOptions {
|
|
17
|
-
instanceAdmin?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface AuthLogoutOptions extends BaseClientOptions {}
|
|
21
|
-
interface AuthWhoamiOptions extends BaseClientOptions {}
|
|
22
|
-
|
|
23
|
-
export function registerClientAuthCommands(auth: Command): void {
|
|
24
|
-
addCommonClientOptions(
|
|
25
|
-
auth
|
|
26
|
-
.command("login")
|
|
27
|
-
.description("Authenticate the CLI for board-user access")
|
|
28
|
-
.option("--instance-admin", "Request instance-admin approval instead of plain board access", false)
|
|
29
|
-
.action(async (opts: AuthLoginOptions) => {
|
|
30
|
-
try {
|
|
31
|
-
const ctx = resolveCommandContext(opts);
|
|
32
|
-
const login = await loginBoardCli({
|
|
33
|
-
apiBase: ctx.api.apiBase,
|
|
34
|
-
requestedAccess: opts.instanceAdmin ? "instance_admin_required" : "board",
|
|
35
|
-
requestedCompanyId: ctx.companyId ?? null,
|
|
36
|
-
command: "paperclipai auth login",
|
|
37
|
-
});
|
|
38
|
-
printOutput(
|
|
39
|
-
{
|
|
40
|
-
ok: true,
|
|
41
|
-
apiBase: ctx.api.apiBase,
|
|
42
|
-
userId: login.userId ?? null,
|
|
43
|
-
approvalUrl: login.approvalUrl,
|
|
44
|
-
},
|
|
45
|
-
{ json: ctx.json },
|
|
46
|
-
);
|
|
47
|
-
} catch (err) {
|
|
48
|
-
handleCommandError(err);
|
|
49
|
-
}
|
|
50
|
-
}),
|
|
51
|
-
{ includeCompany: true },
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
addCommonClientOptions(
|
|
55
|
-
auth
|
|
56
|
-
.command("logout")
|
|
57
|
-
.description("Remove the stored board-user credential for this API base")
|
|
58
|
-
.action(async (opts: AuthLogoutOptions) => {
|
|
59
|
-
try {
|
|
60
|
-
const ctx = resolveCommandContext(opts);
|
|
61
|
-
const credential = getStoredBoardCredential(ctx.api.apiBase);
|
|
62
|
-
if (!credential) {
|
|
63
|
-
printOutput({ ok: true, apiBase: ctx.api.apiBase, revoked: false, removedLocalCredential: false }, { json: ctx.json });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
let revoked = false;
|
|
67
|
-
try {
|
|
68
|
-
await revokeStoredBoardCredential({
|
|
69
|
-
apiBase: ctx.api.apiBase,
|
|
70
|
-
token: credential.token,
|
|
71
|
-
});
|
|
72
|
-
revoked = true;
|
|
73
|
-
} catch {
|
|
74
|
-
// Remove the local credential even if the server-side revoke fails.
|
|
75
|
-
}
|
|
76
|
-
const removedLocalCredential = removeStoredBoardCredential(ctx.api.apiBase);
|
|
77
|
-
printOutput(
|
|
78
|
-
{
|
|
79
|
-
ok: true,
|
|
80
|
-
apiBase: ctx.api.apiBase,
|
|
81
|
-
revoked,
|
|
82
|
-
removedLocalCredential,
|
|
83
|
-
},
|
|
84
|
-
{ json: ctx.json },
|
|
85
|
-
);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
handleCommandError(err);
|
|
88
|
-
}
|
|
89
|
-
}),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
addCommonClientOptions(
|
|
93
|
-
auth
|
|
94
|
-
.command("whoami")
|
|
95
|
-
.description("Show the current board-user identity for this API base")
|
|
96
|
-
.action(async (opts: AuthWhoamiOptions) => {
|
|
97
|
-
try {
|
|
98
|
-
const ctx = resolveCommandContext(opts);
|
|
99
|
-
const me = await ctx.api.get<{
|
|
100
|
-
user: { id: string; name: string; email: string } | null;
|
|
101
|
-
userId: string;
|
|
102
|
-
isInstanceAdmin: boolean;
|
|
103
|
-
companyIds: string[];
|
|
104
|
-
source: string;
|
|
105
|
-
keyId: string | null;
|
|
106
|
-
}>("/api/cli-auth/me");
|
|
107
|
-
printOutput(me, { json: ctx.json });
|
|
108
|
-
} catch (err) {
|
|
109
|
-
handleCommandError(err);
|
|
110
|
-
}
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { getStoredBoardCredential, loginBoardCli } from "../../client/board-auth.js";
|
|
4
|
-
import { buildCliCommandLabel } from "../../client/command-label.js";
|
|
5
|
-
import { readConfig } from "../../config/store.js";
|
|
6
|
-
import { readContext, resolveProfile, type ClientContextProfile } from "../../client/context.js";
|
|
7
|
-
import { ApiRequestError, PaperclipApiClient } from "../../client/http.js";
|
|
8
|
-
|
|
9
|
-
export interface BaseClientOptions {
|
|
10
|
-
config?: string;
|
|
11
|
-
dataDir?: string;
|
|
12
|
-
context?: string;
|
|
13
|
-
profile?: string;
|
|
14
|
-
apiBase?: string;
|
|
15
|
-
apiKey?: string;
|
|
16
|
-
companyId?: string;
|
|
17
|
-
json?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ResolvedClientContext {
|
|
21
|
-
api: PaperclipApiClient;
|
|
22
|
-
companyId?: string;
|
|
23
|
-
profileName: string;
|
|
24
|
-
profile: ClientContextProfile;
|
|
25
|
-
json: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function addCommonClientOptions(command: Command, opts?: { includeCompany?: boolean }): Command {
|
|
29
|
-
command
|
|
30
|
-
.option("-c, --config <path>", "Path to Paperclip config file")
|
|
31
|
-
.option("-d, --data-dir <path>", "Paperclip data directory root (isolates state from ~/.paperclip)")
|
|
32
|
-
.option("--context <path>", "Path to CLI context file")
|
|
33
|
-
.option("--profile <name>", "CLI context profile name")
|
|
34
|
-
.option("--api-base <url>", "Base URL for the Paperclip API")
|
|
35
|
-
.option("--api-key <token>", "Bearer token for agent-authenticated calls")
|
|
36
|
-
.option("--json", "Output raw JSON");
|
|
37
|
-
|
|
38
|
-
if (opts?.includeCompany) {
|
|
39
|
-
command.option("-C, --company-id <id>", "Company ID (overrides context default)");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return command;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function resolveCommandContext(
|
|
46
|
-
options: BaseClientOptions,
|
|
47
|
-
opts?: { requireCompany?: boolean },
|
|
48
|
-
): ResolvedClientContext {
|
|
49
|
-
const context = readContext(options.context);
|
|
50
|
-
const { name: profileName, profile } = resolveProfile(context, options.profile);
|
|
51
|
-
|
|
52
|
-
const apiBase =
|
|
53
|
-
options.apiBase?.trim() ||
|
|
54
|
-
process.env.PAPERCLIP_API_URL?.trim() ||
|
|
55
|
-
profile.apiBase ||
|
|
56
|
-
inferApiBaseFromConfig(options.config);
|
|
57
|
-
|
|
58
|
-
const explicitApiKey =
|
|
59
|
-
options.apiKey?.trim() ||
|
|
60
|
-
process.env.PAPERCLIP_API_KEY?.trim() ||
|
|
61
|
-
readKeyFromProfileEnv(profile);
|
|
62
|
-
const storedBoardCredential = explicitApiKey ? null : getStoredBoardCredential(apiBase);
|
|
63
|
-
const apiKey = explicitApiKey || storedBoardCredential?.token;
|
|
64
|
-
|
|
65
|
-
const companyId =
|
|
66
|
-
options.companyId?.trim() ||
|
|
67
|
-
process.env.PAPERCLIP_COMPANY_ID?.trim() ||
|
|
68
|
-
profile.companyId;
|
|
69
|
-
|
|
70
|
-
if (opts?.requireCompany && !companyId) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
"Company ID is required. Pass --company-id, set PAPERCLIP_COMPANY_ID, or set context profile companyId via `paperclipai context set`.",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const api = new PaperclipApiClient({
|
|
77
|
-
apiBase,
|
|
78
|
-
apiKey,
|
|
79
|
-
recoverAuth: explicitApiKey || !canAttemptInteractiveBoardAuth()
|
|
80
|
-
? undefined
|
|
81
|
-
: async ({ error }) => {
|
|
82
|
-
const requestedAccess = error.message.includes("Instance admin required")
|
|
83
|
-
? "instance_admin_required"
|
|
84
|
-
: "board";
|
|
85
|
-
if (!shouldRecoverBoardAuth(error)) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
const login = await loginBoardCli({
|
|
89
|
-
apiBase,
|
|
90
|
-
requestedAccess,
|
|
91
|
-
requestedCompanyId: companyId ?? null,
|
|
92
|
-
command: buildCliCommandLabel(),
|
|
93
|
-
});
|
|
94
|
-
return login.token;
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
return {
|
|
98
|
-
api,
|
|
99
|
-
companyId,
|
|
100
|
-
profileName,
|
|
101
|
-
profile,
|
|
102
|
-
json: Boolean(options.json),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function shouldRecoverBoardAuth(error: ApiRequestError): boolean {
|
|
107
|
-
if (error.status === 401) return true;
|
|
108
|
-
if (error.status !== 403) return false;
|
|
109
|
-
return error.message.includes("Board access required") || error.message.includes("Instance admin required");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function canAttemptInteractiveBoardAuth(): boolean {
|
|
113
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function printOutput(data: unknown, opts: { json?: boolean; label?: string } = {}): void {
|
|
117
|
-
if (opts.json) {
|
|
118
|
-
console.log(JSON.stringify(data, null, 2));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (opts.label) {
|
|
123
|
-
console.log(pc.bold(opts.label));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (Array.isArray(data)) {
|
|
127
|
-
if (data.length === 0) {
|
|
128
|
-
console.log(pc.dim("(empty)"));
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
for (const item of data) {
|
|
132
|
-
if (typeof item === "object" && item !== null) {
|
|
133
|
-
console.log(formatInlineRecord(item as Record<string, unknown>));
|
|
134
|
-
} else {
|
|
135
|
-
console.log(String(item));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (typeof data === "object" && data !== null) {
|
|
142
|
-
console.log(JSON.stringify(data, null, 2));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (data === undefined || data === null) {
|
|
147
|
-
console.log(pc.dim("(null)"));
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(String(data));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function formatInlineRecord(record: Record<string, unknown>): string {
|
|
155
|
-
const keyOrder = ["identifier", "id", "name", "status", "priority", "title", "action"];
|
|
156
|
-
const seen = new Set<string>();
|
|
157
|
-
const parts: string[] = [];
|
|
158
|
-
|
|
159
|
-
for (const key of keyOrder) {
|
|
160
|
-
if (!(key in record)) continue;
|
|
161
|
-
parts.push(`${key}=${renderValue(record[key])}`);
|
|
162
|
-
seen.add(key);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (const [key, value] of Object.entries(record)) {
|
|
166
|
-
if (seen.has(key)) continue;
|
|
167
|
-
if (typeof value === "object") continue;
|
|
168
|
-
parts.push(`${key}=${renderValue(value)}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return parts.join(" ");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function renderValue(value: unknown): string {
|
|
175
|
-
if (value === null || value === undefined) return "-";
|
|
176
|
-
if (typeof value === "string") {
|
|
177
|
-
const compact = value.replace(/\s+/g, " ").trim();
|
|
178
|
-
return compact.length > 90 ? `${compact.slice(0, 87)}...` : compact;
|
|
179
|
-
}
|
|
180
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
181
|
-
return String(value);
|
|
182
|
-
}
|
|
183
|
-
return "[object]";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function inferApiBaseFromConfig(configPath?: string): string {
|
|
187
|
-
const envHost = process.env.PAPERCLIP_SERVER_HOST?.trim() || "localhost";
|
|
188
|
-
let port = Number(process.env.PAPERCLIP_SERVER_PORT || "");
|
|
189
|
-
|
|
190
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
191
|
-
try {
|
|
192
|
-
const config = readConfig(configPath);
|
|
193
|
-
port = Number(config?.server?.port ?? 3100);
|
|
194
|
-
} catch {
|
|
195
|
-
port = 3100;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
200
|
-
port = 3100;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return `http://${envHost}:${port}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function readKeyFromProfileEnv(profile: ClientContextProfile): string | undefined {
|
|
207
|
-
if (!profile.apiKeyEnvVarName) return undefined;
|
|
208
|
-
return process.env[profile.apiKeyEnvVarName]?.trim() || undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function handleCommandError(error: unknown): never {
|
|
212
|
-
if (error instanceof ApiRequestError) {
|
|
213
|
-
const detailSuffix = error.details !== undefined ? ` details=${JSON.stringify(error.details)}` : "";
|
|
214
|
-
console.error(pc.red(`API error ${error.status}: ${error.message}${detailSuffix}`));
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
-
console.error(pc.red(message));
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|