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.
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 +18 -11
  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,221 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { isLoopbackHost, type BindMode } from "@paperclipai/shared";
3
- import type { AuthConfig, ServerConfig } from "../config/schema.js";
4
- import { parseHostnameCsv } from "../config/hostnames.js";
5
- import { buildCustomServerConfig, buildPresetServerConfig, inferConfiguredBind } from "../config/server-bind.js";
6
-
7
- const TAILNET_BIND_WARNING =
8
- "No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
9
-
10
- function cancelled(): never {
11
- p.cancel("Setup cancelled.");
12
- process.exit(0);
13
- }
14
-
15
- export async function promptServer(opts?: {
16
- currentServer?: Partial<ServerConfig>;
17
- currentAuth?: Partial<AuthConfig>;
18
- }): Promise<{ server: ServerConfig; auth: AuthConfig }> {
19
- const currentServer = opts?.currentServer;
20
- const currentAuth = opts?.currentAuth;
21
- const currentBind = inferConfiguredBind(currentServer);
22
-
23
- const bindSelection = await p.select({
24
- message: "Reachability",
25
- options: [
26
- {
27
- value: "loopback" as const,
28
- label: "Trusted local",
29
- hint: "Recommended for first run: localhost only, no login friction",
30
- },
31
- {
32
- value: "lan" as const,
33
- label: "Private network",
34
- hint: "Broad private bind for LAN, VPN, or legacy --tailscale-auth style access",
35
- },
36
- {
37
- value: "tailnet" as const,
38
- label: "Tailnet",
39
- hint: "Private authenticated access using the machine's detected Tailscale address",
40
- },
41
- {
42
- value: "custom" as const,
43
- label: "Custom",
44
- hint: "Choose exact auth mode, exposure, and host manually",
45
- },
46
- ],
47
- initialValue: currentBind,
48
- });
49
-
50
- if (p.isCancel(bindSelection)) cancelled();
51
- const bind = bindSelection as BindMode;
52
-
53
- const portStr = await p.text({
54
- message: "Server port",
55
- defaultValue: String(currentServer?.port ?? 3100),
56
- placeholder: "3100",
57
- validate: (val) => {
58
- const n = Number(val);
59
- if (isNaN(n) || n < 1 || n > 65535 || !Number.isInteger(n)) {
60
- return "Must be an integer between 1 and 65535";
61
- }
62
- },
63
- });
64
-
65
- if (p.isCancel(portStr)) cancelled();
66
- const port = Number(portStr) || 3100;
67
- const serveUi = currentServer?.serveUi ?? true;
68
-
69
- if (bind === "loopback") {
70
- return buildPresetServerConfig("loopback", {
71
- port,
72
- allowedHostnames: [],
73
- serveUi,
74
- });
75
- }
76
-
77
- if (bind === "lan" || bind === "tailnet") {
78
- const allowedHostnamesInput = await p.text({
79
- message: "Allowed private hostnames (comma-separated, optional)",
80
- defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
81
- placeholder:
82
- bind === "tailnet"
83
- ? "your-machine.tailnet.ts.net"
84
- : "dotta-macbook-pro, host.docker.internal",
85
- validate: (val) => {
86
- try {
87
- parseHostnameCsv(val);
88
- return;
89
- } catch (err) {
90
- return err instanceof Error ? err.message : "Invalid hostname list";
91
- }
92
- },
93
- });
94
-
95
- if (p.isCancel(allowedHostnamesInput)) cancelled();
96
-
97
- const preset = buildPresetServerConfig(bind, {
98
- port,
99
- allowedHostnames: parseHostnameCsv(allowedHostnamesInput),
100
- serveUi,
101
- });
102
- if (bind === "tailnet" && isLoopbackHost(preset.server.host)) {
103
- p.log.warn(TAILNET_BIND_WARNING);
104
- }
105
- return preset;
106
- }
107
-
108
- const deploymentModeSelection = await p.select({
109
- message: "Auth mode",
110
- options: [
111
- {
112
- value: "local_trusted",
113
- label: "Local trusted",
114
- hint: "No login required; only safe with loopback-only or similarly trusted access",
115
- },
116
- {
117
- value: "authenticated",
118
- label: "Authenticated",
119
- hint: "Login required; supports both private-network and public deployments",
120
- },
121
- ],
122
- initialValue: currentServer?.deploymentMode ?? "authenticated",
123
- });
124
-
125
- if (p.isCancel(deploymentModeSelection)) cancelled();
126
- const deploymentMode = deploymentModeSelection as ServerConfig["deploymentMode"];
127
-
128
- let exposure: ServerConfig["exposure"] = "private";
129
- if (deploymentMode === "authenticated") {
130
- const exposureSelection = await p.select({
131
- message: "Exposure profile",
132
- options: [
133
- {
134
- value: "private",
135
- label: "Private network",
136
- hint: "Private access only, with automatic URL handling",
137
- },
138
- {
139
- value: "public",
140
- label: "Public internet",
141
- hint: "Internet-facing deployment with explicit public URL requirements",
142
- },
143
- ],
144
- initialValue: currentServer?.exposure ?? "private",
145
- });
146
- if (p.isCancel(exposureSelection)) cancelled();
147
- exposure = exposureSelection as ServerConfig["exposure"];
148
- }
149
-
150
- const defaultHost =
151
- currentServer?.customBindHost ??
152
- currentServer?.host ??
153
- (deploymentMode === "local_trusted" ? "127.0.0.1" : "0.0.0.0");
154
- const host = await p.text({
155
- message: "Bind host",
156
- defaultValue: defaultHost,
157
- placeholder: defaultHost,
158
- validate: (val) => {
159
- if (!val.trim()) return "Host is required";
160
- if (deploymentMode === "local_trusted" && !isLoopbackHost(val.trim())) {
161
- return "Local trusted mode requires a loopback host such as 127.0.0.1";
162
- }
163
- },
164
- });
165
-
166
- if (p.isCancel(host)) cancelled();
167
-
168
- let allowedHostnames: string[] = [];
169
- if (deploymentMode === "authenticated" && exposure === "private") {
170
- const allowedHostnamesInput = await p.text({
171
- message: "Allowed private hostnames (comma-separated, optional)",
172
- defaultValue: (currentServer?.allowedHostnames ?? []).join(", "),
173
- placeholder: "dotta-macbook-pro, your-host.tailnet.ts.net",
174
- validate: (val) => {
175
- try {
176
- parseHostnameCsv(val);
177
- return;
178
- } catch (err) {
179
- return err instanceof Error ? err.message : "Invalid hostname list";
180
- }
181
- },
182
- });
183
-
184
- if (p.isCancel(allowedHostnamesInput)) cancelled();
185
- allowedHostnames = parseHostnameCsv(allowedHostnamesInput);
186
- }
187
-
188
- let publicBaseUrl: string | undefined;
189
- if (deploymentMode === "authenticated" && exposure === "public") {
190
- const urlInput = await p.text({
191
- message: "Public base URL",
192
- defaultValue: currentAuth?.publicBaseUrl ?? "",
193
- placeholder: "https://paperclip.example.com",
194
- validate: (val) => {
195
- const candidate = val.trim();
196
- if (!candidate) return "Public base URL is required for public exposure";
197
- try {
198
- const url = new URL(candidate);
199
- if (url.protocol !== "http:" && url.protocol !== "https:") {
200
- return "URL must start with http:// or https://";
201
- }
202
- return;
203
- } catch {
204
- return "Enter a valid URL";
205
- }
206
- },
207
- });
208
- if (p.isCancel(urlInput)) cancelled();
209
- publicBaseUrl = urlInput.trim().replace(/\/+$/, "");
210
- }
211
-
212
- return buildCustomServerConfig({
213
- deploymentMode,
214
- exposure,
215
- host: host.trim(),
216
- port,
217
- allowedHostnames,
218
- serveUi,
219
- publicBaseUrl,
220
- });
221
- }
@@ -1,146 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import type { StorageConfig } from "../config/schema.js";
3
- import { resolveDefaultStorageDir, resolvePaperclipInstanceId } from "../config/home.js";
4
-
5
- function defaultStorageBaseDir(): string {
6
- return resolveDefaultStorageDir(resolvePaperclipInstanceId());
7
- }
8
-
9
- export function defaultStorageConfig(): StorageConfig {
10
- return {
11
- provider: "local_disk",
12
- localDisk: {
13
- baseDir: defaultStorageBaseDir(),
14
- },
15
- s3: {
16
- bucket: "paperclip",
17
- region: "us-east-1",
18
- endpoint: undefined,
19
- prefix: "",
20
- forcePathStyle: false,
21
- },
22
- };
23
- }
24
-
25
- export async function promptStorage(current?: StorageConfig): Promise<StorageConfig> {
26
- const base = current ?? defaultStorageConfig();
27
-
28
- const provider = await p.select({
29
- message: "Storage provider",
30
- options: [
31
- {
32
- value: "local_disk" as const,
33
- label: "Local disk (recommended)",
34
- hint: "best for single-user local deployments",
35
- },
36
- {
37
- value: "s3" as const,
38
- label: "S3 compatible",
39
- hint: "for cloud/object storage backends",
40
- },
41
- ],
42
- initialValue: base.provider,
43
- });
44
-
45
- if (p.isCancel(provider)) {
46
- p.cancel("Setup cancelled.");
47
- process.exit(0);
48
- }
49
-
50
- if (provider === "local_disk") {
51
- const baseDir = await p.text({
52
- message: "Local storage base directory",
53
- defaultValue: base.localDisk.baseDir || defaultStorageBaseDir(),
54
- placeholder: defaultStorageBaseDir(),
55
- validate: (value) => {
56
- if (!value || value.trim().length === 0) return "Storage base directory is required";
57
- },
58
- });
59
-
60
- if (p.isCancel(baseDir)) {
61
- p.cancel("Setup cancelled.");
62
- process.exit(0);
63
- }
64
-
65
- return {
66
- provider: "local_disk",
67
- localDisk: {
68
- baseDir: baseDir.trim(),
69
- },
70
- s3: base.s3,
71
- };
72
- }
73
-
74
- const bucket = await p.text({
75
- message: "S3 bucket",
76
- defaultValue: base.s3.bucket || "paperclip",
77
- placeholder: "paperclip",
78
- validate: (value) => {
79
- if (!value || value.trim().length === 0) return "Bucket is required";
80
- },
81
- });
82
-
83
- if (p.isCancel(bucket)) {
84
- p.cancel("Setup cancelled.");
85
- process.exit(0);
86
- }
87
-
88
- const region = await p.text({
89
- message: "S3 region",
90
- defaultValue: base.s3.region || "us-east-1",
91
- placeholder: "us-east-1",
92
- validate: (value) => {
93
- if (!value || value.trim().length === 0) return "Region is required";
94
- },
95
- });
96
-
97
- if (p.isCancel(region)) {
98
- p.cancel("Setup cancelled.");
99
- process.exit(0);
100
- }
101
-
102
- const endpoint = await p.text({
103
- message: "S3 endpoint (optional for compatible backends)",
104
- defaultValue: base.s3.endpoint ?? "",
105
- placeholder: "https://s3.amazonaws.com",
106
- });
107
-
108
- if (p.isCancel(endpoint)) {
109
- p.cancel("Setup cancelled.");
110
- process.exit(0);
111
- }
112
-
113
- const prefix = await p.text({
114
- message: "Object key prefix (optional)",
115
- defaultValue: base.s3.prefix ?? "",
116
- placeholder: "paperclip/",
117
- });
118
-
119
- if (p.isCancel(prefix)) {
120
- p.cancel("Setup cancelled.");
121
- process.exit(0);
122
- }
123
-
124
- const forcePathStyle = await p.confirm({
125
- message: "Use S3 path-style URLs?",
126
- initialValue: base.s3.forcePathStyle ?? false,
127
- });
128
-
129
- if (p.isCancel(forcePathStyle)) {
130
- p.cancel("Setup cancelled.");
131
- process.exit(0);
132
- }
133
-
134
- return {
135
- provider: "s3",
136
- localDisk: base.localDisk,
137
- s3: {
138
- bucket: bucket.trim(),
139
- region: region.trim(),
140
- endpoint: endpoint.trim() || undefined,
141
- prefix: prefix.trim(),
142
- forcePathStyle,
143
- },
144
- };
145
- }
146
-
package/src/telemetry.ts DELETED
@@ -1,49 +0,0 @@
1
- import path from "node:path";
2
- import {
3
- TelemetryClient,
4
- resolveTelemetryConfig,
5
- loadOrCreateState,
6
- trackInstallStarted,
7
- trackInstallCompleted,
8
- trackCompanyImported,
9
- } from "../../packages/shared/src/telemetry/index.js";
10
- import { resolvePaperclipInstanceRoot } from "./config/home.js";
11
- import { readConfig } from "./config/store.js";
12
- import { cliVersion } from "./version.js";
13
-
14
- let client: TelemetryClient | null = null;
15
-
16
- export function initTelemetry(fileConfig?: { enabled?: boolean }): TelemetryClient | null {
17
- if (client) return client;
18
-
19
- const config = resolveTelemetryConfig(fileConfig);
20
- if (!config.enabled) return null;
21
-
22
- const stateDir = path.join(resolvePaperclipInstanceRoot(), "telemetry");
23
- client = new TelemetryClient(config, () => loadOrCreateState(stateDir, cliVersion), cliVersion);
24
- return client;
25
- }
26
-
27
- export function initTelemetryFromConfigFile(configPath?: string): TelemetryClient | null {
28
- try {
29
- return initTelemetry(readConfig(configPath)?.telemetry);
30
- } catch {
31
- return initTelemetry();
32
- }
33
- }
34
-
35
- export function getTelemetryClient(): TelemetryClient | null {
36
- return client;
37
- }
38
-
39
- export async function flushTelemetry(): Promise<void> {
40
- if (client) {
41
- await client.flush();
42
- }
43
- }
44
-
45
- export {
46
- trackInstallStarted,
47
- trackInstallCompleted,
48
- trackCompanyImported,
49
- };
@@ -1,24 +0,0 @@
1
- import pc from "picocolors";
2
-
3
- const PAPERCLIP_ART = [
4
- "██████╗ █████╗ ██████╗ ███████╗██████╗ ██████╗██╗ ██╗██████╗ ",
5
- "██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝██║ ██║██╔══██╗",
6
- "██████╔╝███████║██████╔╝█████╗ ██████╔╝██║ ██║ ██║██████╔╝",
7
- "██╔═══╝ ██╔══██║██╔═══╝ ██╔══╝ ██╔══██╗██║ ██║ ██║██╔═══╝ ",
8
- "██║ ██║ ██║██║ ███████╗██║ ██║╚██████╗███████╗██║██║ ",
9
- "╚═╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝╚═╝ ",
10
- ] as const;
11
-
12
- const TAGLINE = "Open-source orchestration for zero-human companies";
13
-
14
- export function printPaperclipCliBanner(): void {
15
- const lines = [
16
- "",
17
- ...PAPERCLIP_ART.map((line) => pc.cyan(line)),
18
- pc.blue(" ───────────────────────────────────────────────────────"),
19
- pc.bold(pc.white(` ${TAGLINE}`)),
20
- "",
21
- ];
22
-
23
- console.log(lines.join("\n"));
24
- }
package/src/utils/net.ts DELETED
@@ -1,18 +0,0 @@
1
- import net from "node:net";
2
-
3
- export function checkPort(port: number): Promise<{ available: boolean; error?: string }> {
4
- return new Promise((resolve) => {
5
- const server = net.createServer();
6
- server.once("error", (err: NodeJS.ErrnoException) => {
7
- if (err.code === "EADDRINUSE") {
8
- resolve({ available: false, error: `Port ${port} is already in use` });
9
- } else {
10
- resolve({ available: false, error: err.message });
11
- }
12
- });
13
- server.once("listening", () => {
14
- server.close(() => resolve({ available: true }));
15
- });
16
- server.listen(port, "127.0.0.1");
17
- });
18
- }
@@ -1,25 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { expandHomePrefix } from "../config/home.js";
4
-
5
- function unique(items: string[]): string[] {
6
- return Array.from(new Set(items));
7
- }
8
-
9
- export function resolveRuntimeLikePath(value: string, configPath?: string): string {
10
- const expanded = expandHomePrefix(value);
11
- if (path.isAbsolute(expanded)) return path.resolve(expanded);
12
-
13
- const cwd = process.cwd();
14
- const configDir = configPath ? path.dirname(configPath) : null;
15
- const workspaceRoot = configDir ? path.resolve(configDir, "..") : cwd;
16
-
17
- const candidates = unique([
18
- ...(configDir ? [path.resolve(configDir, expanded)] : []),
19
- path.resolve(workspaceRoot, "server", expanded),
20
- path.resolve(workspaceRoot, expanded),
21
- path.resolve(cwd, expanded),
22
- ]);
23
-
24
- return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
25
- }
package/src/version.ts DELETED
@@ -1,10 +0,0 @@
1
- import { createRequire } from "node:module";
2
-
3
- type PackageJson = {
4
- version?: string;
5
- };
6
-
7
- const require = createRequire(import.meta.url);
8
- const pkg = require("../package.json") as PackageJson;
9
-
10
- export const cliVersion = pkg.version ?? "0.0.0";