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,79 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
- import { applyDataDirOverride } from "../config/data-dir.js";
5
-
6
- const ORIGINAL_ENV = { ...process.env };
7
-
8
- describe("applyDataDirOverride", () => {
9
- beforeEach(() => {
10
- process.env = { ...ORIGINAL_ENV };
11
- delete process.env.PAPERCLIP_HOME;
12
- delete process.env.PAPERCLIP_CONFIG;
13
- delete process.env.PAPERCLIP_CONTEXT;
14
- delete process.env.PAPERCLIP_INSTANCE_ID;
15
- });
16
-
17
- afterEach(() => {
18
- process.env = { ...ORIGINAL_ENV };
19
- });
20
-
21
- it("sets PAPERCLIP_HOME and isolated default config/context paths", () => {
22
- const home = applyDataDirOverride({
23
- dataDir: "~/paperclip-data",
24
- config: undefined,
25
- context: undefined,
26
- }, { hasConfigOption: true, hasContextOption: true });
27
-
28
- const expectedHome = path.resolve(os.homedir(), "paperclip-data");
29
- expect(home).toBe(expectedHome);
30
- expect(process.env.PAPERCLIP_HOME).toBe(expectedHome);
31
- expect(process.env.PAPERCLIP_CONFIG).toBe(
32
- path.resolve(expectedHome, "instances", "default", "config.json"),
33
- );
34
- expect(process.env.PAPERCLIP_CONTEXT).toBe(path.resolve(expectedHome, "context.json"));
35
- expect(process.env.PAPERCLIP_INSTANCE_ID).toBe("default");
36
- });
37
-
38
- it("uses the provided instance id when deriving default config path", () => {
39
- const home = applyDataDirOverride({
40
- dataDir: "/tmp/paperclip-alt",
41
- instance: "dev_1",
42
- config: undefined,
43
- context: undefined,
44
- }, { hasConfigOption: true, hasContextOption: true });
45
-
46
- expect(home).toBe(path.resolve("/tmp/paperclip-alt"));
47
- expect(process.env.PAPERCLIP_INSTANCE_ID).toBe("dev_1");
48
- expect(process.env.PAPERCLIP_CONFIG).toBe(
49
- path.resolve("/tmp/paperclip-alt", "instances", "dev_1", "config.json"),
50
- );
51
- });
52
-
53
- it("does not override explicit config/context settings", () => {
54
- process.env.PAPERCLIP_CONFIG = "/env/config.json";
55
- process.env.PAPERCLIP_CONTEXT = "/env/context.json";
56
-
57
- applyDataDirOverride({
58
- dataDir: "/tmp/paperclip-alt",
59
- config: "/flag/config.json",
60
- context: "/flag/context.json",
61
- }, { hasConfigOption: true, hasContextOption: true });
62
-
63
- expect(process.env.PAPERCLIP_CONFIG).toBe("/env/config.json");
64
- expect(process.env.PAPERCLIP_CONTEXT).toBe("/env/context.json");
65
- });
66
-
67
- it("only applies defaults for options supported by the command", () => {
68
- applyDataDirOverride(
69
- {
70
- dataDir: "/tmp/paperclip-alt",
71
- },
72
- { hasConfigOption: false, hasContextOption: false },
73
- );
74
-
75
- expect(process.env.PAPERCLIP_HOME).toBe(path.resolve("/tmp/paperclip-alt"));
76
- expect(process.env.PAPERCLIP_CONFIG).toBeUndefined();
77
- expect(process.env.PAPERCLIP_CONTEXT).toBeUndefined();
78
- });
79
- });
@@ -1,102 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { doctor } from "../commands/doctor.js";
6
- import { writeConfig } from "../config/store.js";
7
- import type { PaperclipConfig } from "../config/schema.js";
8
-
9
- const ORIGINAL_ENV = { ...process.env };
10
-
11
- function createTempConfig(): string {
12
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-doctor-"));
13
- const configPath = path.join(root, ".paperclip", "config.json");
14
- const runtimeRoot = path.join(root, "runtime");
15
-
16
- const config: PaperclipConfig = {
17
- $meta: {
18
- version: 1,
19
- updatedAt: "2026-03-10T00:00:00.000Z",
20
- source: "configure",
21
- },
22
- database: {
23
- mode: "embedded-postgres",
24
- embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
25
- embeddedPostgresPort: 55432,
26
- backup: {
27
- enabled: true,
28
- intervalMinutes: 60,
29
- retentionDays: 30,
30
- dir: path.join(runtimeRoot, "backups"),
31
- },
32
- },
33
- logging: {
34
- mode: "file",
35
- logDir: path.join(runtimeRoot, "logs"),
36
- },
37
- server: {
38
- deploymentMode: "local_trusted",
39
- exposure: "private",
40
- host: "127.0.0.1",
41
- port: 3199,
42
- allowedHostnames: [],
43
- serveUi: true,
44
- },
45
- auth: {
46
- baseUrlMode: "auto",
47
- disableSignUp: false,
48
- },
49
- telemetry: {
50
- enabled: true,
51
- },
52
- storage: {
53
- provider: "local_disk",
54
- localDisk: {
55
- baseDir: path.join(runtimeRoot, "storage"),
56
- },
57
- s3: {
58
- bucket: "paperclip",
59
- region: "us-east-1",
60
- prefix: "",
61
- forcePathStyle: false,
62
- },
63
- },
64
- secrets: {
65
- provider: "local_encrypted",
66
- strictMode: false,
67
- localEncrypted: {
68
- keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
69
- },
70
- },
71
- };
72
-
73
- writeConfig(config, configPath);
74
- return configPath;
75
- }
76
-
77
- describe("doctor", () => {
78
- beforeEach(() => {
79
- process.env = { ...ORIGINAL_ENV };
80
- delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
81
- delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
82
- delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
83
- });
84
-
85
- afterEach(() => {
86
- process.env = { ...ORIGINAL_ENV };
87
- });
88
-
89
- it("re-runs repairable checks so repaired failures do not remain blocking", async () => {
90
- const configPath = createTempConfig();
91
-
92
- const summary = await doctor({
93
- config: configPath,
94
- repair: true,
95
- yes: true,
96
- });
97
-
98
- expect(summary.failed).toBe(0);
99
- expect(summary.warned).toBe(0);
100
- expect(process.env.PAPERCLIP_AGENT_JWT_SECRET).toBeTruthy();
101
- });
102
- });
@@ -1,177 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { mkdtemp, readFile } from "node:fs/promises";
4
- import { Command } from "commander";
5
- import { describe, expect, it } from "vitest";
6
- import type { FeedbackTrace } from "@paperclipai/shared";
7
- import { readZipArchive } from "../commands/client/zip.js";
8
- import {
9
- buildFeedbackTraceQuery,
10
- registerFeedbackCommands,
11
- renderFeedbackReport,
12
- summarizeFeedbackTraces,
13
- writeFeedbackExportBundle,
14
- } from "../commands/client/feedback.js";
15
-
16
- function makeTrace(overrides: Partial<FeedbackTrace> = {}): FeedbackTrace {
17
- return {
18
- id: "trace-12345678",
19
- companyId: "company-123",
20
- feedbackVoteId: "vote-12345678",
21
- issueId: "issue-123",
22
- projectId: "project-123",
23
- issueIdentifier: "PAP-123",
24
- issueTitle: "Fix the feedback command",
25
- authorUserId: "user-123",
26
- targetType: "issue_comment",
27
- targetId: "comment-123",
28
- vote: "down",
29
- status: "pending",
30
- destination: "paperclip_labs_feedback_v1",
31
- exportId: null,
32
- consentVersion: "feedback-data-sharing-v1",
33
- schemaVersion: "1",
34
- bundleVersion: "1",
35
- payloadVersion: "1",
36
- payloadDigest: null,
37
- payloadSnapshot: {
38
- vote: {
39
- value: "down",
40
- reason: "Needed more detail",
41
- },
42
- },
43
- targetSummary: {
44
- label: "Comment",
45
- excerpt: "The first answer was too vague.",
46
- authorAgentId: "agent-123",
47
- authorUserId: null,
48
- createdAt: new Date("2026-03-31T12:00:00.000Z"),
49
- documentKey: null,
50
- documentTitle: null,
51
- revisionNumber: null,
52
- },
53
- redactionSummary: null,
54
- attemptCount: 0,
55
- lastAttemptedAt: null,
56
- exportedAt: null,
57
- failureReason: null,
58
- createdAt: new Date("2026-03-31T12:01:00.000Z"),
59
- updatedAt: new Date("2026-03-31T12:02:00.000Z"),
60
- ...overrides,
61
- };
62
- }
63
-
64
- describe("registerFeedbackCommands", () => {
65
- it("registers the top-level feedback commands", () => {
66
- const program = new Command();
67
-
68
- expect(() => registerFeedbackCommands(program)).not.toThrow();
69
-
70
- const feedback = program.commands.find((command) => command.name() === "feedback");
71
- expect(feedback).toBeDefined();
72
- expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]);
73
- expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1);
74
- });
75
- });
76
-
77
- describe("buildFeedbackTraceQuery", () => {
78
- it("encodes all supported filters", () => {
79
- expect(
80
- buildFeedbackTraceQuery({
81
- targetType: "issue_comment",
82
- vote: "down",
83
- status: "pending",
84
- projectId: "project-123",
85
- issueId: "issue-123",
86
- from: "2026-03-31T00:00:00.000Z",
87
- to: "2026-03-31T23:59:59.999Z",
88
- sharedOnly: true,
89
- }),
90
- ).toBe(
91
- "?targetType=issue_comment&vote=down&status=pending&projectId=project-123&issueId=issue-123&from=2026-03-31T00%3A00%3A00.000Z&to=2026-03-31T23%3A59%3A59.999Z&sharedOnly=true&includePayload=true",
92
- );
93
- });
94
- });
95
-
96
- describe("renderFeedbackReport", () => {
97
- it("includes summary counts and the optional reason", () => {
98
- const traces = [
99
- makeTrace(),
100
- makeTrace({
101
- id: "trace-87654321",
102
- feedbackVoteId: "vote-87654321",
103
- vote: "up",
104
- status: "local_only",
105
- payloadSnapshot: {
106
- vote: {
107
- value: "up",
108
- reason: null,
109
- },
110
- },
111
- }),
112
- ];
113
-
114
- const report = renderFeedbackReport({
115
- apiBase: "http://127.0.0.1:3100",
116
- companyId: "company-123",
117
- traces,
118
- summary: summarizeFeedbackTraces(traces),
119
- includePayloads: false,
120
- });
121
-
122
- expect(report).toContain("Paperclip Feedback Report");
123
- expect(report).toContain("thumbs up");
124
- expect(report).toContain("thumbs down");
125
- expect(report).toContain("Needed more detail");
126
- });
127
- });
128
-
129
- describe("writeFeedbackExportBundle", () => {
130
- it("writes votes, traces, a manifest, and a zip archive", async () => {
131
- const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-feedback-export-"));
132
- const outputDir = path.join(tempDir, "feedback-export");
133
- const traces = [
134
- makeTrace(),
135
- makeTrace({
136
- id: "trace-abcdef12",
137
- feedbackVoteId: "vote-abcdef12",
138
- issueIdentifier: "PAP-124",
139
- issueId: "issue-124",
140
- vote: "up",
141
- status: "local_only",
142
- payloadSnapshot: {
143
- vote: {
144
- value: "up",
145
- reason: null,
146
- },
147
- },
148
- }),
149
- ];
150
-
151
- const exported = await writeFeedbackExportBundle({
152
- apiBase: "http://127.0.0.1:3100",
153
- companyId: "company-123",
154
- traces,
155
- outputDir,
156
- });
157
-
158
- expect(exported.manifest.summary.total).toBe(2);
159
- expect(exported.manifest.summary.withReason).toBe(1);
160
-
161
- const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as {
162
- files: { votes: string[]; traces: string[]; zip: string };
163
- };
164
- expect(manifest.files.votes).toHaveLength(2);
165
- expect(manifest.files.traces).toHaveLength(2);
166
-
167
- const archive = await readFile(exported.zipPath);
168
- const zip = await readZipArchive(archive);
169
- expect(Object.keys(zip.files)).toEqual(
170
- expect.arrayContaining([
171
- "index.json",
172
- `votes/${manifest.files.votes[0]}`,
173
- `traces/${manifest.files.traces[0]}`,
174
- ]),
175
- );
176
- });
177
- });
@@ -1,6 +0,0 @@
1
- export {
2
- getEmbeddedPostgresTestSupport,
3
- startEmbeddedPostgresTestDatabase,
4
- type EmbeddedPostgresTestDatabase,
5
- type EmbeddedPostgresTestSupport,
6
- } from "@paperclipai/db";
@@ -1,87 +0,0 @@
1
- function writeUint16(target: Uint8Array, offset: number, value: number) {
2
- target[offset] = value & 0xff;
3
- target[offset + 1] = (value >>> 8) & 0xff;
4
- }
5
-
6
- function writeUint32(target: Uint8Array, offset: number, value: number) {
7
- target[offset] = value & 0xff;
8
- target[offset + 1] = (value >>> 8) & 0xff;
9
- target[offset + 2] = (value >>> 16) & 0xff;
10
- target[offset + 3] = (value >>> 24) & 0xff;
11
- }
12
-
13
- function crc32(bytes: Uint8Array) {
14
- let crc = 0xffffffff;
15
- for (const byte of bytes) {
16
- crc ^= byte;
17
- for (let bit = 0; bit < 8; bit += 1) {
18
- crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
19
- }
20
- }
21
- return (crc ^ 0xffffffff) >>> 0;
22
- }
23
-
24
- export function createStoredZipArchive(files: Record<string, string>, rootPath: string) {
25
- const encoder = new TextEncoder();
26
- const localChunks: Uint8Array[] = [];
27
- const centralChunks: Uint8Array[] = [];
28
- let localOffset = 0;
29
- let entryCount = 0;
30
-
31
- for (const [relativePath, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) {
32
- const fileName = encoder.encode(`${rootPath}/${relativePath}`);
33
- const body = encoder.encode(content);
34
- const checksum = crc32(body);
35
-
36
- const localHeader = new Uint8Array(30 + fileName.length);
37
- writeUint32(localHeader, 0, 0x04034b50);
38
- writeUint16(localHeader, 4, 20);
39
- writeUint16(localHeader, 6, 0x0800);
40
- writeUint16(localHeader, 8, 0);
41
- writeUint32(localHeader, 14, checksum);
42
- writeUint32(localHeader, 18, body.length);
43
- writeUint32(localHeader, 22, body.length);
44
- writeUint16(localHeader, 26, fileName.length);
45
- localHeader.set(fileName, 30);
46
-
47
- const centralHeader = new Uint8Array(46 + fileName.length);
48
- writeUint32(centralHeader, 0, 0x02014b50);
49
- writeUint16(centralHeader, 4, 20);
50
- writeUint16(centralHeader, 6, 20);
51
- writeUint16(centralHeader, 8, 0x0800);
52
- writeUint16(centralHeader, 10, 0);
53
- writeUint32(centralHeader, 16, checksum);
54
- writeUint32(centralHeader, 20, body.length);
55
- writeUint32(centralHeader, 24, body.length);
56
- writeUint16(centralHeader, 28, fileName.length);
57
- writeUint32(centralHeader, 42, localOffset);
58
- centralHeader.set(fileName, 46);
59
-
60
- localChunks.push(localHeader, body);
61
- centralChunks.push(centralHeader);
62
- localOffset += localHeader.length + body.length;
63
- entryCount += 1;
64
- }
65
-
66
- const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0);
67
- const archive = new Uint8Array(
68
- localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22,
69
- );
70
- let offset = 0;
71
- for (const chunk of localChunks) {
72
- archive.set(chunk, offset);
73
- offset += chunk.length;
74
- }
75
- const centralDirectoryOffset = offset;
76
- for (const chunk of centralChunks) {
77
- archive.set(chunk, offset);
78
- offset += chunk.length;
79
- }
80
- writeUint32(archive, offset, 0x06054b50);
81
- writeUint16(archive, offset + 8, entryCount);
82
- writeUint16(archive, offset + 10, entryCount);
83
- writeUint32(archive, offset + 12, centralDirectoryLength);
84
- writeUint32(archive, offset + 16, centralDirectoryOffset);
85
-
86
- return archive;
87
- }
@@ -1,44 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { afterEach, describe, expect, it } from "vitest";
4
- import {
5
- describeLocalInstancePaths,
6
- expandHomePrefix,
7
- resolvePaperclipHomeDir,
8
- resolvePaperclipInstanceId,
9
- } from "../config/home.js";
10
-
11
- const ORIGINAL_ENV = { ...process.env };
12
-
13
- describe("home path resolution", () => {
14
- afterEach(() => {
15
- process.env = { ...ORIGINAL_ENV };
16
- });
17
-
18
- it("defaults to ~/.paperclip and default instance", () => {
19
- delete process.env.PAPERCLIP_HOME;
20
- delete process.env.PAPERCLIP_INSTANCE_ID;
21
-
22
- const paths = describeLocalInstancePaths();
23
- expect(paths.homeDir).toBe(path.resolve(os.homedir(), ".paperclip"));
24
- expect(paths.instanceId).toBe("default");
25
- expect(paths.configPath).toBe(path.resolve(os.homedir(), ".paperclip", "instances", "default", "config.json"));
26
- });
27
-
28
- it("supports PAPERCLIP_HOME and explicit instance ids", () => {
29
- process.env.PAPERCLIP_HOME = "~/paperclip-home";
30
-
31
- const home = resolvePaperclipHomeDir();
32
- expect(home).toBe(path.resolve(os.homedir(), "paperclip-home"));
33
- expect(resolvePaperclipInstanceId("dev_1")).toBe("dev_1");
34
- });
35
-
36
- it("rejects invalid instance ids", () => {
37
- expect(() => resolvePaperclipInstanceId("bad/id")).toThrow(/Invalid instance id/);
38
- });
39
-
40
- it("expands ~ prefixes", () => {
41
- expect(expandHomePrefix("~")).toBe(os.homedir());
42
- expect(expandHomePrefix("~/x/y")).toBe(path.resolve(os.homedir(), "x/y"));
43
- });
44
- });
@@ -1,106 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { ApiConnectionError, ApiRequestError, PaperclipApiClient } from "../client/http.js";
3
-
4
- describe("PaperclipApiClient", () => {
5
- afterEach(() => {
6
- vi.restoreAllMocks();
7
- });
8
-
9
- it("adds authorization and run-id headers", async () => {
10
- const fetchMock = vi.fn().mockResolvedValue(
11
- new Response(JSON.stringify({ ok: true }), { status: 200 }),
12
- );
13
- vi.stubGlobal("fetch", fetchMock);
14
-
15
- const client = new PaperclipApiClient({
16
- apiBase: "http://localhost:3100",
17
- apiKey: "token-123",
18
- runId: "run-abc",
19
- });
20
-
21
- await client.post("/api/test", { hello: "world" });
22
-
23
- expect(fetchMock).toHaveBeenCalledTimes(1);
24
- const call = fetchMock.mock.calls[0] as [string, RequestInit];
25
- expect(call[0]).toContain("/api/test");
26
-
27
- const headers = call[1].headers as Record<string, string>;
28
- expect(headers.authorization).toBe("Bearer token-123");
29
- expect(headers["x-paperclip-run-id"]).toBe("run-abc");
30
- expect(headers["content-type"]).toBe("application/json");
31
- });
32
-
33
- it("returns null on ignoreNotFound", async () => {
34
- const fetchMock = vi.fn().mockResolvedValue(
35
- new Response(JSON.stringify({ error: "Not found" }), { status: 404 }),
36
- );
37
- vi.stubGlobal("fetch", fetchMock);
38
-
39
- const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
40
- const result = await client.get("/api/missing", { ignoreNotFound: true });
41
- expect(result).toBeNull();
42
- });
43
-
44
- it("throws ApiRequestError with details", async () => {
45
- const fetchMock = vi.fn().mockResolvedValue(
46
- new Response(
47
- JSON.stringify({ error: "Issue checkout conflict", details: { issueId: "1" } }),
48
- { status: 409 },
49
- ),
50
- );
51
- vi.stubGlobal("fetch", fetchMock);
52
-
53
- const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
54
-
55
- await expect(client.post("/api/issues/1/checkout", {})).rejects.toMatchObject({
56
- status: 409,
57
- message: "Issue checkout conflict",
58
- details: { issueId: "1" },
59
- } satisfies Partial<ApiRequestError>);
60
- });
61
-
62
- it("throws ApiConnectionError with recovery guidance when fetch fails", async () => {
63
- const fetchMock = vi.fn().mockRejectedValue(new TypeError("fetch failed"));
64
- vi.stubGlobal("fetch", fetchMock);
65
-
66
- const client = new PaperclipApiClient({ apiBase: "http://localhost:3100" });
67
-
68
- await expect(client.post("/api/companies/import/preview", {})).rejects.toBeInstanceOf(ApiConnectionError);
69
- await expect(client.post("/api/companies/import/preview", {})).rejects.toMatchObject({
70
- url: "http://localhost:3100/api/companies/import/preview",
71
- method: "POST",
72
- causeMessage: "fetch failed",
73
- } satisfies Partial<ApiConnectionError>);
74
- await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
75
- /Could not reach the Paperclip API\./,
76
- );
77
- await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
78
- /curl http:\/\/localhost:3100\/api\/health/,
79
- );
80
- await expect(client.post("/api/companies/import/preview", {})).rejects.toThrow(
81
- /pnpm dev|pnpm paperclipai run/,
82
- );
83
- });
84
-
85
- it("retries once after interactive auth recovery", async () => {
86
- const fetchMock = vi
87
- .fn()
88
- .mockResolvedValueOnce(new Response(JSON.stringify({ error: "Board access required" }), { status: 403 }))
89
- .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
90
- vi.stubGlobal("fetch", fetchMock);
91
-
92
- const recoverAuth = vi.fn().mockResolvedValue("board-token-123");
93
- const client = new PaperclipApiClient({
94
- apiBase: "http://localhost:3100",
95
- recoverAuth,
96
- });
97
-
98
- const result = await client.post<{ ok: boolean }>("/api/test", { hello: "world" });
99
-
100
- expect(result).toEqual({ ok: true });
101
- expect(recoverAuth).toHaveBeenCalledOnce();
102
- expect(fetchMock).toHaveBeenCalledTimes(2);
103
- const retryHeaders = fetchMock.mock.calls[1]?.[1]?.headers as Record<string, string>;
104
- expect(retryHeaders.authorization).toBe("Bearer board-token-123");
105
- });
106
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveRuntimeBind, validateConfiguredBindMode } from "@paperclipai/shared";
3
- import { buildPresetServerConfig } from "../config/server-bind.js";
4
-
5
- describe("network bind helpers", () => {
6
- it("rejects non-loopback bind modes in local_trusted", () => {
7
- expect(
8
- validateConfiguredBindMode({
9
- deploymentMode: "local_trusted",
10
- deploymentExposure: "private",
11
- bind: "lan",
12
- host: "0.0.0.0",
13
- }),
14
- ).toContain("local_trusted requires server.bind=loopback");
15
- });
16
-
17
- it("resolves tailnet bind using the detected tailscale address", () => {
18
- const resolved = resolveRuntimeBind({
19
- bind: "tailnet",
20
- host: "127.0.0.1",
21
- tailnetBindHost: "100.64.0.8",
22
- });
23
-
24
- expect(resolved.errors).toEqual([]);
25
- expect(resolved.host).toBe("100.64.0.8");
26
- });
27
-
28
- it("requires a custom bind host when bind=custom", () => {
29
- const resolved = resolveRuntimeBind({
30
- bind: "custom",
31
- host: "127.0.0.1",
32
- });
33
-
34
- expect(resolved.errors).toContain("server.customBindHost is required when server.bind=custom");
35
- });
36
-
37
- it("stores the detected tailscale address for tailnet presets", () => {
38
- process.env.PAPERCLIP_TAILNET_BIND_HOST = "100.64.0.8";
39
-
40
- const preset = buildPresetServerConfig("tailnet", {
41
- port: 3100,
42
- allowedHostnames: [],
43
- serveUi: true,
44
- });
45
-
46
- expect(preset.server.host).toBe("100.64.0.8");
47
-
48
- delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
49
- });
50
-
51
- it("falls back to loopback when no tailscale address is available for tailnet presets", () => {
52
- delete process.env.PAPERCLIP_TAILNET_BIND_HOST;
53
-
54
- const preset = buildPresetServerConfig("tailnet", {
55
- port: 3100,
56
- allowedHostnames: [],
57
- serveUi: true,
58
- });
59
-
60
- expect(preset.server.host).toBe("127.0.0.1");
61
- });
62
- });