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.
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 +18 -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,4 +0,0 @@
1
- export function printHttpStdoutEvent(raw: string, _debug: boolean): void {
2
- const line = raw.trim();
3
- if (line) console.log(line);
4
- }
@@ -1,7 +0,0 @@
1
- import type { CLIAdapterModule } from "@paperclipai/adapter-utils";
2
- import { printHttpStdoutEvent } from "./format-event.js";
3
-
4
- export const httpCLIAdapter: CLIAdapterModule = {
5
- type: "http",
6
- formatStdoutEvent: printHttpStdoutEvent,
7
- };
@@ -1,2 +0,0 @@
1
- export { getCLIAdapter } from "./registry.js";
2
- export type { CLIAdapterModule } from "@paperclipai/adapter-utils";
@@ -1,4 +0,0 @@
1
- export function printProcessStdoutEvent(raw: string, _debug: boolean): void {
2
- const line = raw.trim();
3
- if (line) console.log(line);
4
- }
@@ -1,7 +0,0 @@
1
- import type { CLIAdapterModule } from "@paperclipai/adapter-utils";
2
- import { printProcessStdoutEvent } from "./format-event.js";
3
-
4
- export const processCLIAdapter: CLIAdapterModule = {
5
- type: "process",
6
- formatStdoutEvent: printProcessStdoutEvent,
7
- };
@@ -1,63 +0,0 @@
1
- import type { CLIAdapterModule } from "@paperclipai/adapter-utils";
2
- import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli";
3
- import { printCodexStreamEvent } from "@paperclipai/adapter-codex-local/cli";
4
- import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
5
- import { printGeminiStreamEvent } from "@paperclipai/adapter-gemini-local/cli";
6
- import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli";
7
- import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli";
8
- import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-gateway/cli";
9
- import { processCLIAdapter } from "./process/index.js";
10
- import { httpCLIAdapter } from "./http/index.js";
11
-
12
- const claudeLocalCLIAdapter: CLIAdapterModule = {
13
- type: "claude_local",
14
- formatStdoutEvent: printClaudeStreamEvent,
15
- };
16
-
17
- const codexLocalCLIAdapter: CLIAdapterModule = {
18
- type: "codex_local",
19
- formatStdoutEvent: printCodexStreamEvent,
20
- };
21
-
22
- const openCodeLocalCLIAdapter: CLIAdapterModule = {
23
- type: "opencode_local",
24
- formatStdoutEvent: printOpenCodeStreamEvent,
25
- };
26
-
27
- const piLocalCLIAdapter: CLIAdapterModule = {
28
- type: "pi_local",
29
- formatStdoutEvent: printPiStreamEvent,
30
- };
31
-
32
- const cursorLocalCLIAdapter: CLIAdapterModule = {
33
- type: "cursor",
34
- formatStdoutEvent: printCursorStreamEvent,
35
- };
36
-
37
- const geminiLocalCLIAdapter: CLIAdapterModule = {
38
- type: "gemini_local",
39
- formatStdoutEvent: printGeminiStreamEvent,
40
- };
41
-
42
- const openclawGatewayCLIAdapter: CLIAdapterModule = {
43
- type: "openclaw_gateway",
44
- formatStdoutEvent: printOpenClawGatewayStreamEvent,
45
- };
46
-
47
- const adaptersByType = new Map<string, CLIAdapterModule>(
48
- [
49
- claudeLocalCLIAdapter,
50
- codexLocalCLIAdapter,
51
- openCodeLocalCLIAdapter,
52
- piLocalCLIAdapter,
53
- cursorLocalCLIAdapter,
54
- geminiLocalCLIAdapter,
55
- openclawGatewayCLIAdapter,
56
- processCLIAdapter,
57
- httpCLIAdapter,
58
- ].map((a) => [a.type, a]),
59
- );
60
-
61
- export function getCLIAdapter(type: string): CLIAdapterModule {
62
- return adaptersByType.get(type) ?? processCLIAdapter;
63
- }
@@ -1,40 +0,0 @@
1
- import {
2
- ensureAgentJwtSecret,
3
- readAgentJwtSecretFromEnv,
4
- readAgentJwtSecretFromEnvFile,
5
- resolveAgentJwtEnvFile,
6
- } from "../config/env.js";
7
- import type { CheckResult } from "./index.js";
8
-
9
- export function agentJwtSecretCheck(configPath?: string): CheckResult {
10
- if (readAgentJwtSecretFromEnv(configPath)) {
11
- return {
12
- name: "Agent JWT secret",
13
- status: "pass",
14
- message: "PAPERCLIP_AGENT_JWT_SECRET is set in environment",
15
- };
16
- }
17
-
18
- const envPath = resolveAgentJwtEnvFile(configPath);
19
- const fileSecret = readAgentJwtSecretFromEnvFile(envPath);
20
-
21
- if (fileSecret) {
22
- return {
23
- name: "Agent JWT secret",
24
- status: "warn",
25
- message: `PAPERCLIP_AGENT_JWT_SECRET is present in ${envPath} but not loaded into environment`,
26
- repairHint: `Set the value from ${envPath} in your shell before starting the Paperclip server`,
27
- };
28
- }
29
-
30
- return {
31
- name: "Agent JWT secret",
32
- status: "fail",
33
- message: `PAPERCLIP_AGENT_JWT_SECRET missing from environment and ${envPath}`,
34
- canRepair: true,
35
- repair: () => {
36
- ensureAgentJwtSecret(configPath);
37
- },
38
- repairHint: `Run with --repair to create ${envPath} containing PAPERCLIP_AGENT_JWT_SECRET`,
39
- };
40
- }
@@ -1,33 +0,0 @@
1
- import { readConfig, configExists, resolveConfigPath } from "../config/store.js";
2
- import type { CheckResult } from "./index.js";
3
-
4
- export function configCheck(configPath?: string): CheckResult {
5
- const filePath = resolveConfigPath(configPath);
6
-
7
- if (!configExists(configPath)) {
8
- return {
9
- name: "Config file",
10
- status: "fail",
11
- message: `Config file not found at ${filePath}`,
12
- canRepair: false,
13
- repairHint: "Run `paperclipai onboard` to create one",
14
- };
15
- }
16
-
17
- try {
18
- readConfig(configPath);
19
- return {
20
- name: "Config file",
21
- status: "pass",
22
- message: `Valid config at ${filePath}`,
23
- };
24
- } catch (err) {
25
- return {
26
- name: "Config file",
27
- status: "fail",
28
- message: `Invalid config: ${err instanceof Error ? err.message : String(err)}`,
29
- canRepair: false,
30
- repairHint: "Run `paperclipai configure --section database` (or `paperclipai onboard` to recreate)",
31
- };
32
- }
33
- }
@@ -1,59 +0,0 @@
1
- import fs from "node:fs";
2
- import type { PaperclipConfig } from "../config/schema.js";
3
- import type { CheckResult } from "./index.js";
4
- import { resolveRuntimeLikePath } from "./path-resolver.js";
5
-
6
- export async function databaseCheck(config: PaperclipConfig, configPath?: string): Promise<CheckResult> {
7
- if (config.database.mode === "postgres") {
8
- if (!config.database.connectionString) {
9
- return {
10
- name: "Database",
11
- status: "fail",
12
- message: "PostgreSQL mode selected but no connection string configured",
13
- canRepair: false,
14
- repairHint: "Run `paperclipai configure --section database`",
15
- };
16
- }
17
-
18
- try {
19
- const { createDb } = await import("@paperclipai/db");
20
- const db = createDb(config.database.connectionString);
21
- await db.execute("SELECT 1");
22
- return {
23
- name: "Database",
24
- status: "pass",
25
- message: "PostgreSQL connection successful",
26
- };
27
- } catch (err) {
28
- return {
29
- name: "Database",
30
- status: "fail",
31
- message: `Cannot connect to PostgreSQL: ${err instanceof Error ? err.message : String(err)}`,
32
- canRepair: false,
33
- repairHint: "Check your connection string and ensure PostgreSQL is running",
34
- };
35
- }
36
- }
37
-
38
- if (config.database.mode === "embedded-postgres") {
39
- const dataDir = resolveRuntimeLikePath(config.database.embeddedPostgresDataDir, configPath);
40
- const reportedPath = dataDir;
41
- if (!fs.existsSync(dataDir)) {
42
- fs.mkdirSync(reportedPath, { recursive: true });
43
- }
44
-
45
- return {
46
- name: "Database",
47
- status: "pass",
48
- message: `Embedded PostgreSQL configured at ${dataDir} (port ${config.database.embeddedPostgresPort})`,
49
- };
50
- }
51
-
52
- return {
53
- name: "Database",
54
- status: "fail",
55
- message: `Unknown database mode: ${String(config.database.mode)}`,
56
- canRepair: false,
57
- repairHint: "Run `paperclipai configure --section database`",
58
- };
59
- }
@@ -1,88 +0,0 @@
1
- import { inferBindModeFromHost } from "@paperclipai/shared";
2
- import type { PaperclipConfig } from "../config/schema.js";
3
- import type { CheckResult } from "./index.js";
4
-
5
- export function deploymentAuthCheck(config: PaperclipConfig): CheckResult {
6
- const mode = config.server.deploymentMode;
7
- const exposure = config.server.exposure;
8
- const auth = config.auth;
9
- const bind = config.server.bind ?? inferBindModeFromHost(config.server.host);
10
-
11
- if (mode === "local_trusted") {
12
- if (bind !== "loopback") {
13
- return {
14
- name: "Deployment/auth mode",
15
- status: "fail",
16
- message: `local_trusted requires loopback binding (found ${bind})`,
17
- canRepair: false,
18
- repairHint: "Run `paperclipai configure --section server` and choose Local trusted / loopback reachability",
19
- };
20
- }
21
- return {
22
- name: "Deployment/auth mode",
23
- status: "pass",
24
- message: "local_trusted mode is configured for loopback-only access",
25
- };
26
- }
27
-
28
- const secret =
29
- process.env.BETTER_AUTH_SECRET?.trim() ??
30
- process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim();
31
- if (!secret) {
32
- return {
33
- name: "Deployment/auth mode",
34
- status: "fail",
35
- message: "authenticated mode requires BETTER_AUTH_SECRET (or PAPERCLIP_AGENT_JWT_SECRET)",
36
- canRepair: false,
37
- repairHint: "Set BETTER_AUTH_SECRET before starting Paperclip",
38
- };
39
- }
40
-
41
- if (auth.baseUrlMode === "explicit" && !auth.publicBaseUrl) {
42
- return {
43
- name: "Deployment/auth mode",
44
- status: "fail",
45
- message: "auth.baseUrlMode=explicit requires auth.publicBaseUrl",
46
- canRepair: false,
47
- repairHint: "Run `paperclipai configure --section server` and provide a base URL",
48
- };
49
- }
50
-
51
- if (exposure === "public") {
52
- if (auth.baseUrlMode !== "explicit" || !auth.publicBaseUrl) {
53
- return {
54
- name: "Deployment/auth mode",
55
- status: "fail",
56
- message: "authenticated/public requires explicit auth.publicBaseUrl",
57
- canRepair: false,
58
- repairHint: "Run `paperclipai configure --section server` and select public exposure",
59
- };
60
- }
61
- try {
62
- const url = new URL(auth.publicBaseUrl);
63
- if (url.protocol !== "https:") {
64
- return {
65
- name: "Deployment/auth mode",
66
- status: "warn",
67
- message: "Public exposure should use an https:// auth.publicBaseUrl",
68
- canRepair: false,
69
- repairHint: "Use HTTPS in production for secure session cookies",
70
- };
71
- }
72
- } catch {
73
- return {
74
- name: "Deployment/auth mode",
75
- status: "fail",
76
- message: "auth.publicBaseUrl is not a valid URL",
77
- canRepair: false,
78
- repairHint: "Run `paperclipai configure --section server` and provide a valid URL",
79
- };
80
- }
81
- }
82
-
83
- return {
84
- name: "Deployment/auth mode",
85
- status: "pass",
86
- message: `Mode ${mode}/${exposure} with bind ${bind} and auth URL mode ${auth.baseUrlMode}`,
87
- };
88
- }
@@ -1,18 +0,0 @@
1
- export interface CheckResult {
2
- name: string;
3
- status: "pass" | "warn" | "fail";
4
- message: string;
5
- canRepair?: boolean;
6
- repair?: () => void | Promise<void>;
7
- repairHint?: string;
8
- }
9
-
10
- export { agentJwtSecretCheck } from "./agent-jwt-secret-check.js";
11
- export { configCheck } from "./config-check.js";
12
- export { deploymentAuthCheck } from "./deployment-auth-check.js";
13
- export { databaseCheck } from "./database-check.js";
14
- export { llmCheck } from "./llm-check.js";
15
- export { logCheck } from "./log-check.js";
16
- export { portCheck } from "./port-check.js";
17
- export { secretsCheck } from "./secrets-check.js";
18
- export { storageCheck } from "./storage-check.js";
@@ -1,82 +0,0 @@
1
- import type { PaperclipConfig } from "../config/schema.js";
2
- import type { CheckResult } from "./index.js";
3
-
4
- export async function llmCheck(config: PaperclipConfig): Promise<CheckResult> {
5
- if (!config.llm) {
6
- return {
7
- name: "LLM provider",
8
- status: "pass",
9
- message: "No LLM provider configured (optional)",
10
- };
11
- }
12
-
13
- if (!config.llm.apiKey) {
14
- return {
15
- name: "LLM provider",
16
- status: "pass",
17
- message: `${config.llm.provider} configured but no API key set (optional)`,
18
- };
19
- }
20
-
21
- try {
22
- if (config.llm.provider === "claude") {
23
- const res = await fetch("https://api.anthropic.com/v1/messages", {
24
- method: "POST",
25
- headers: {
26
- "x-api-key": config.llm.apiKey,
27
- "anthropic-version": "2023-06-01",
28
- "content-type": "application/json",
29
- },
30
- body: JSON.stringify({
31
- model: "claude-sonnet-4-5-20250929",
32
- max_tokens: 1,
33
- messages: [{ role: "user", content: "hi" }],
34
- }),
35
- });
36
- if (res.ok || res.status === 400) {
37
- return { name: "LLM provider", status: "pass", message: "Claude API key is valid" };
38
- }
39
- if (res.status === 401) {
40
- return {
41
- name: "LLM provider",
42
- status: "fail",
43
- message: "Claude API key is invalid (401)",
44
- canRepair: false,
45
- repairHint: "Run `paperclipai configure --section llm`",
46
- };
47
- }
48
- return {
49
- name: "LLM provider",
50
- status: "warn",
51
- message: `Claude API returned status ${res.status}`,
52
- };
53
- } else {
54
- const res = await fetch("https://api.openai.com/v1/models", {
55
- headers: { Authorization: `Bearer ${config.llm.apiKey}` },
56
- });
57
- if (res.ok) {
58
- return { name: "LLM provider", status: "pass", message: "OpenAI API key is valid" };
59
- }
60
- if (res.status === 401) {
61
- return {
62
- name: "LLM provider",
63
- status: "fail",
64
- message: "OpenAI API key is invalid (401)",
65
- canRepair: false,
66
- repairHint: "Run `paperclipai configure --section llm`",
67
- };
68
- }
69
- return {
70
- name: "LLM provider",
71
- status: "warn",
72
- message: `OpenAI API returned status ${res.status}`,
73
- };
74
- }
75
- } catch {
76
- return {
77
- name: "LLM provider",
78
- status: "warn",
79
- message: "Could not reach API to validate key",
80
- };
81
- }
82
- }
@@ -1,30 +0,0 @@
1
- import fs from "node:fs";
2
- import type { PaperclipConfig } from "../config/schema.js";
3
- import type { CheckResult } from "./index.js";
4
- import { resolveRuntimeLikePath } from "./path-resolver.js";
5
-
6
- export function logCheck(config: PaperclipConfig, configPath?: string): CheckResult {
7
- const logDir = resolveRuntimeLikePath(config.logging.logDir, configPath);
8
- const reportedDir = logDir;
9
-
10
- if (!fs.existsSync(logDir)) {
11
- fs.mkdirSync(reportedDir, { recursive: true });
12
- }
13
-
14
- try {
15
- fs.accessSync(reportedDir, fs.constants.W_OK);
16
- return {
17
- name: "Log directory",
18
- status: "pass",
19
- message: `Log directory is writable: ${reportedDir}`,
20
- };
21
- } catch {
22
- return {
23
- name: "Log directory",
24
- status: "fail",
25
- message: `Log directory is not writable: ${logDir}`,
26
- canRepair: false,
27
- repairHint: "Check file permissions on the log directory",
28
- };
29
- }
30
- }
@@ -1 +0,0 @@
1
- export { resolveRuntimeLikePath } from "../utils/path-resolver.js";
@@ -1,24 +0,0 @@
1
- import type { PaperclipConfig } from "../config/schema.js";
2
- import { checkPort } from "../utils/net.js";
3
- import type { CheckResult } from "./index.js";
4
-
5
- export async function portCheck(config: PaperclipConfig): Promise<CheckResult> {
6
- const port = config.server.port;
7
- const result = await checkPort(port);
8
-
9
- if (result.available) {
10
- return {
11
- name: "Server port",
12
- status: "pass",
13
- message: `Port ${port} is available`,
14
- };
15
- }
16
-
17
- return {
18
- name: "Server port",
19
- status: "warn",
20
- message: result.error ?? `Port ${port} is not available`,
21
- canRepair: false,
22
- repairHint: `Check what's using port ${port} with: lsof -i :${port}`,
23
- };
24
- }
@@ -1,146 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import type { PaperclipConfig } from "../config/schema.js";
5
- import type { CheckResult } from "./index.js";
6
- import { resolveRuntimeLikePath } from "./path-resolver.js";
7
-
8
- function decodeMasterKey(raw: string): Buffer | null {
9
- const trimmed = raw.trim();
10
- if (!trimmed) return null;
11
-
12
- if (/^[A-Fa-f0-9]{64}$/.test(trimmed)) {
13
- return Buffer.from(trimmed, "hex");
14
- }
15
-
16
- try {
17
- const decoded = Buffer.from(trimmed, "base64");
18
- if (decoded.length === 32) return decoded;
19
- } catch {
20
- // ignored
21
- }
22
-
23
- if (Buffer.byteLength(trimmed, "utf8") === 32) {
24
- return Buffer.from(trimmed, "utf8");
25
- }
26
- return null;
27
- }
28
-
29
- function withStrictModeNote(
30
- base: Pick<CheckResult, "name" | "status" | "message" | "canRepair" | "repair" | "repairHint">,
31
- config: PaperclipConfig,
32
- ): CheckResult {
33
- const strictModeDisabledInDeployedSetup =
34
- config.database.mode === "postgres" && config.secrets.strictMode === false;
35
- if (!strictModeDisabledInDeployedSetup) return base;
36
-
37
- if (base.status === "fail") return base;
38
- return {
39
- ...base,
40
- status: "warn",
41
- message: `${base.message}; strict secret mode is disabled for postgres deployment`,
42
- repairHint: base.repairHint
43
- ? `${base.repairHint}. Consider enabling secrets.strictMode`
44
- : "Consider enabling secrets.strictMode",
45
- };
46
- }
47
-
48
- export function secretsCheck(config: PaperclipConfig, configPath?: string): CheckResult {
49
- const provider = config.secrets.provider;
50
- if (provider !== "local_encrypted") {
51
- return {
52
- name: "Secrets adapter",
53
- status: "fail",
54
- message: `${provider} is configured, but this build only supports local_encrypted`,
55
- canRepair: false,
56
- repairHint: "Run `paperclipai configure --section secrets` and set provider to local_encrypted",
57
- };
58
- }
59
-
60
- const envMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
61
- if (envMasterKey && envMasterKey.trim().length > 0) {
62
- if (!decodeMasterKey(envMasterKey)) {
63
- return {
64
- name: "Secrets adapter",
65
- status: "fail",
66
- message:
67
- "PAPERCLIP_SECRETS_MASTER_KEY is invalid (expected 32-byte base64, 64-char hex, or raw 32-char string)",
68
- canRepair: false,
69
- repairHint: "Set PAPERCLIP_SECRETS_MASTER_KEY to a valid key or unset it to use a key file",
70
- };
71
- }
72
-
73
- return withStrictModeNote(
74
- {
75
- name: "Secrets adapter",
76
- status: "pass",
77
- message: "Local encrypted provider configured via PAPERCLIP_SECRETS_MASTER_KEY",
78
- },
79
- config,
80
- );
81
- }
82
-
83
- const keyFileOverride = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
84
- const configuredPath =
85
- keyFileOverride && keyFileOverride.trim().length > 0
86
- ? keyFileOverride.trim()
87
- : config.secrets.localEncrypted.keyFilePath;
88
- const keyFilePath = resolveRuntimeLikePath(configuredPath, configPath);
89
-
90
- if (!fs.existsSync(keyFilePath)) {
91
- return withStrictModeNote(
92
- {
93
- name: "Secrets adapter",
94
- status: "warn",
95
- message: `Secrets key file does not exist yet: ${keyFilePath}`,
96
- canRepair: true,
97
- repair: () => {
98
- fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
99
- fs.writeFileSync(keyFilePath, randomBytes(32).toString("base64"), {
100
- encoding: "utf8",
101
- mode: 0o600,
102
- });
103
- try {
104
- fs.chmodSync(keyFilePath, 0o600);
105
- } catch {
106
- // best effort
107
- }
108
- },
109
- repairHint: "Run with --repair to create a local encrypted secrets key file",
110
- },
111
- config,
112
- );
113
- }
114
-
115
- let raw: string;
116
- try {
117
- raw = fs.readFileSync(keyFilePath, "utf8");
118
- } catch (err) {
119
- return {
120
- name: "Secrets adapter",
121
- status: "fail",
122
- message: `Could not read secrets key file: ${err instanceof Error ? err.message : String(err)}`,
123
- canRepair: false,
124
- repairHint: "Check file permissions or set PAPERCLIP_SECRETS_MASTER_KEY",
125
- };
126
- }
127
-
128
- if (!decodeMasterKey(raw)) {
129
- return {
130
- name: "Secrets adapter",
131
- status: "fail",
132
- message: `Invalid key material in ${keyFilePath}`,
133
- canRepair: false,
134
- repairHint: "Replace with valid key material or delete it and run doctor --repair",
135
- };
136
- }
137
-
138
- return withStrictModeNote(
139
- {
140
- name: "Secrets adapter",
141
- status: "pass",
142
- message: `Local encrypted provider configured with key file ${keyFilePath}`,
143
- },
144
- config,
145
- );
146
- }
@@ -1,51 +0,0 @@
1
- import fs from "node:fs";
2
- import type { PaperclipConfig } from "../config/schema.js";
3
- import type { CheckResult } from "./index.js";
4
- import { resolveRuntimeLikePath } from "./path-resolver.js";
5
-
6
- export function storageCheck(config: PaperclipConfig, configPath?: string): CheckResult {
7
- if (config.storage.provider === "local_disk") {
8
- const baseDir = resolveRuntimeLikePath(config.storage.localDisk.baseDir, configPath);
9
- if (!fs.existsSync(baseDir)) {
10
- fs.mkdirSync(baseDir, { recursive: true });
11
- }
12
-
13
- try {
14
- fs.accessSync(baseDir, fs.constants.W_OK);
15
- return {
16
- name: "Storage",
17
- status: "pass",
18
- message: `Local disk storage is writable: ${baseDir}`,
19
- };
20
- } catch {
21
- return {
22
- name: "Storage",
23
- status: "fail",
24
- message: `Local storage directory is not writable: ${baseDir}`,
25
- canRepair: false,
26
- repairHint: "Check file permissions for storage.localDisk.baseDir",
27
- };
28
- }
29
- }
30
-
31
- const bucket = config.storage.s3.bucket.trim();
32
- const region = config.storage.s3.region.trim();
33
- if (!bucket || !region) {
34
- return {
35
- name: "Storage",
36
- status: "fail",
37
- message: "S3 storage requires non-empty bucket and region",
38
- canRepair: false,
39
- repairHint: "Run `paperclipai configure --section storage`",
40
- };
41
- }
42
-
43
- return {
44
- name: "Storage",
45
- status: "warn",
46
- message: `S3 storage configured (bucket=${bucket}, region=${region}). Reachability check is skipped in doctor.`,
47
- canRepair: false,
48
- repairHint: "Verify credentials and endpoint in deployment environment",
49
- };
50
- }
51
-