Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ launchctl-tmux-session-logger: ## Restart tmux-session-logger launchd agent.
##@ Systemd Services (Linux)

.PHONY: systemctl
systemctl: systemctl-cliproxyapi systemctl-cliproxyapi-backup systemctl-code-syncer systemctl-docker-postgres systemctl-dotfiles-updater systemctl-make-updater systemctl-neverssl-keepalive systemctl-ollama systemctl-openclaw systemctl-tmux-session-logger ## Restart all systemd user services.
systemctl: systemctl-cliproxyapi systemctl-cliproxyapi-backup systemctl-code-syncer systemctl-docker-postgres systemctl-dotfiles-updater systemctl-make-updater systemctl-neverssl-keepalive systemctl-ollama systemctl-openclaw systemctl-paperclip systemctl-tmux-session-logger ## Restart all systemd user services.

.PHONY: systemctl-cliproxyapi
systemctl-cliproxyapi: ## Restart cliproxyapi systemd user service.
Expand Down Expand Up @@ -873,6 +873,16 @@ systemctl-openclaw: ## Restart OpenClaw gateway systemd user service.
fi
@echo "✅ openclaw restarted"

.PHONY: systemctl-paperclip
systemctl-paperclip: ## Restart Paperclip systemd user service.
@echo "🔄 Restarting paperclip..."
@if [ "$(DETECTED_HOST)" = "kyber" ] || [ "$(HOST)" = "kyber" ]; then \
systemctl --user daemon-reload; \
systemctl --user restart paperclip.service; \
else \
echo "Skipping paperclip.service (host not kyber)"; \
fi
@echo "✅ paperclip restarted"
Comment on lines +876 to +885

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Success message is printed even when the service restart is skipped.

The echo "✅ paperclip restarted" on line 885 executes unconditionally, even when the host is not kyber and the restart was skipped. This could be misleading.

🔧 Suggested fix
 .PHONY: systemctl-paperclip
 systemctl-paperclip: ## Restart Paperclip systemd user service.
 	`@echo` "🔄 Restarting paperclip..."
 	`@if` [ "$(DETECTED_HOST)" = "kyber" ] || [ "$(HOST)" = "kyber" ]; then \
 		systemctl --user daemon-reload; \
-		systemctl --user restart paperclip.service; \
+		systemctl --user restart paperclip.service; \
+		echo "✅ paperclip restarted"; \
 	else \
 		echo "Skipping paperclip.service (host not kyber)"; \
 	fi
-	`@echo` "✅ paperclip restarted"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.PHONY: systemctl-paperclip
systemctl-paperclip: ## Restart Paperclip systemd user service.
@echo "🔄 Restarting paperclip..."
@if [ "$(DETECTED_HOST)" = "kyber" ] || [ "$(HOST)" = "kyber" ]; then \
systemctl --user daemon-reload; \
systemctl --user restart paperclip.service; \
else \
echo "Skipping paperclip.service (host not kyber)"; \
fi
@echo "✅ paperclip restarted"
.PHONY: systemctl-paperclip
systemctl-paperclip: ## Restart Paperclip systemd user service.
`@echo` "🔄 Restarting paperclip..."
`@if` [ "$(DETECTED_HOST)" = "kyber" ] || [ "$(HOST)" = "kyber" ]; then \
systemctl --user daemon-reload; \
systemctl --user restart paperclip.service; \
echo "✅ paperclip restarted"; \
else \
echo "Skipping paperclip.service (host not kyber)"; \
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 876 - 885, The success message is printed
unconditionally in the systemctl-paperclip Makefile target; change the logic so
the final echo "✅ paperclip restarted" only runs when the restart branch
executes. Modify the target (systemctl-paperclip) to move or duplicate the
success echo inside the if branch that runs when DETECTED_HOST or HOST equals
"kyber", or add a conditional check around the final echo that tests
"$(DETECTED_HOST)" or "$(HOST)" == "kyber" so the success message is only
emitted when the service restart actually occurred.


.PHONY: systemctl-tmux-session-logger
systemctl-tmux-session-logger: ## Restart tmux-session-logger systemd timer and service.
Expand Down
374 changes: 374 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions config/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ in
./llm
./openclaw
./omp
./paperclip
./opencode
./pi
./serena
Expand Down
42 changes: 42 additions & 0 deletions config/paperclip/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
config,
pkgs,
inputs,
...
}:
let
inherit (inputs) host;
homeDir = config.home.homeDirectory;
instanceDir = "${homeDir}/.paperclip/instances/default";

configJson = builtins.toJSON {
database =
if host.isKyber then
{
mode = "postgres";
connectionString = "postgres://postgres:postgres@localhost:5432/paperclip";
}
else
{
mode = "embedded-postgres";
};
server = {
host = if host.isKyber then "0.0.0.0" else "127.0.0.1";

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On kyber this binds Paperclip to 0.0.0.0, which makes it reachable from any network interface. If the intent is to serve only behind a local reverse proxy (or only via SSH tunnel), consider binding to 127.0.0.1 and letting the proxy handle external exposure; otherwise ensure host firewall / auth is enforced by default.

Suggested change
host = if host.isKyber then "0.0.0.0" else "127.0.0.1";
host = "127.0.0.1";

Copilot uses AI. Check for mistakes.
port = 3100;
};
};

configFile = pkgs.writeText "paperclip-config.json" configJson;

setupScript = pkgs.replaceVars ./setup.sh {
instance_dir = instanceDir;
config_file = "${configFile}";
cp = "${pkgs.coreutils}/bin/cp";
is_kyber = if host.isKyber then "true" else "false";
};
in
{
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
Comment on lines +11 to +24

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connectionString (including username/password) is being written via pkgs.writeText, which places the full JSON (credentials included) in the Nix store where it is generally world-readable. If these credentials are meant to be secret (or even just to avoid baking defaults into the store), move them to an out-of-store secret (e.g., ${instanceDir}/.env/age/sops) and have the config reference env vars or omit the password for local trust/auth.

Suggested change
configJson = builtins.toJSON {
database =
if host.isKyber then
{
mode = "postgres";
connectionString = "postgres://postgres:postgres@localhost:5432/paperclip";
}
else
{
mode = "embedded-postgres";
};
server = {
host = if host.isKyber then "0.0.0.0" else "127.0.0.1";
port = 3100;
};
};
configFile = pkgs.writeText "paperclip-config.json" configJson;
setupScript = pkgs.replaceVars ./setup.sh {
instance_dir = instanceDir;
config_file = "${configFile}";
cp = "${pkgs.coreutils}/bin/cp";
is_kyber = if host.isKyber then "true" else "false";
};
in
{
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
configFile = "${instanceDir}/config.json";
setupScript = pkgs.replaceVars ./setup.sh {
instance_dir = instanceDir;
config_file = configFile;
cp = "${pkgs.coreutils}/bin/cp";
is_kyber = if host.isKyber then "true" else "false";
};
in
{
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
${pkgs.coreutils}/bin/mkdir -p "${instanceDir}"
${
if host.isKyber then
''
if [ -z "''${PAPERCLIP_DATABASE_URL:-}" ]; then
echo "paperclip: PAPERCLIP_DATABASE_URL must be set to create ${configFile}" >&2
exit 1
fi
cat > "${configFile}" <<EOF
{
"database": {
"mode": "postgres",
"connectionString": "''${PAPERCLIP_DATABASE_URL}"
},
"server": {
"host": "0.0.0.0",
"port": 3100
}
}
EOF
''
else
''
cat > "${configFile}" <<EOF
{
"database": {
"mode": "embedded-postgres"
},
"server": {
"host": "127.0.0.1",
"port": 3100
}
}
EOF
''
}

Copilot uses AI. Check for mistakes.
${pkgs.bash}/bin/bash ${setupScript} || true
'';
Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure suppression may hide critical setup errors.

The || true suffix causes all errors from the setup script to be silently ignored. This could mask failures like:

  • Database creation errors
  • Permission issues
  • Missing config file

Consider removing || true or adding explicit error handling with logging.

🔧 Suggested fix with error logging
   home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
-    ${pkgs.bash}/bin/bash ${setupScript} || true
+    ${pkgs.bash}/bin/bash ${setupScript} || echo "Warning: Paperclip setup encountered an error" >&2
   '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
${pkgs.bash}/bin/bash ${setupScript} || true
'';
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
${pkgs.bash}/bin/bash ${setupScript} || echo "Warning: Paperclip setup encountered an error" >&2
'';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/paperclip/default.nix` around lines 39 - 41, The activation script
currently silences all failures by appending "|| true" to the setup invocation
in home.activation.paperclipConfig; remove that suffix so the setupScript errors
propagate (or replace it with explicit error handling that logs the failure and
exits non‑zero), ensuring the ${pkgs.bash}/bin/bash ${setupScript} invocation
returns a failing exit code on error and writes diagnostic output
(stderr/stdout) to logs before exiting so issues like DB creation or permission
errors are visible.

}
21 changes: 21 additions & 0 deletions config/paperclip/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

INSTANCE_DIR="@instance_dir@"
CONFIG="${INSTANCE_DIR}/config.json"

mkdir -p "${INSTANCE_DIR}"

@cp@ "@config_file@" "$CONFIG"
chmod 600 "$CONFIG"

# Create paperclip database on docker-postgres if needed
# shellcheck disable=SC2050 -- @is_kyber@ is substituted by Nix replaceVars
if [ "@is_kyber@" = "true" ]; then
if command -v docker >/dev/null 2>&1 && docker container inspect postgres >/dev/null 2>&1; then
if ! docker exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw paperclip; then
docker exec postgres createdb -U postgres paperclip
echo "Created paperclip database" >&2
fi
fi
fi
1 change: 1 addition & 0 deletions home-manager/modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
./bin-shells
./cargo-globals
./openclaw
./paperclip
./local-binaries
./local-scripts
./npm-globals
Expand Down
50 changes: 50 additions & 0 deletions home-manager/modules/paperclip/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
config,
lib,
inputs,
...
}:
let
inherit (inputs) host;
homeDir = config.home.homeDirectory;
instanceDir = "${homeDir}/.paperclip/instances/default";
in
# Only enable on kyber (server host)
lib.mkIf host.isKyber {
# Ensure Paperclip directories exist with correct permissions
home.activation.paperclipSetup = config.lib.dag.entryAfter [ "writeBoundary" ] ''
mkdir -p /tmp/paperclip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Creating a log directory in /tmp during activation is unreliable because /tmp is cleared on reboot, but activation scripts do not typically run on every boot. This can lead to the service failing to start after a reboot if it expects the directory to exist. It is better to let the service manage its own logs via the journal or use a persistent path.

mkdir -p ${homeDir}/.paperclip
chmod 700 ${homeDir}/.paperclip
Comment on lines +15 to +18

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service logs to /tmp/paperclip/paperclip.log, but /tmp is typically cleared on reboot; since the directory is only created via Home Manager activation, the service can fail to start on the next login/boot when /tmp/paperclip is missing. Prefer a systemd-managed directory (e.g., RuntimeDirectory= / StateDirectory= / LogsDirectory=) or an ExecStartPre that ensures the log directory exists at service start (and set restrictive permissions).

Copilot uses AI. Check for mistakes.
'';

# Systemd service for Paperclip
systemd.user.services.paperclip = {
Unit = {
Description = "Paperclip AI agent orchestration platform";
After = [
"network-online.target"
"docker-postgres.service"
];
Wants = [ "network-online.target" ];
Requires = [ "docker-postgres.service" ];
};
Service = {
Type = "simple";
ExecStart = "${homeDir}/.bun/bin/paperclipai run";
Restart = "always";
RestartSec = "5s";
Environment = [
"HOME=${homeDir}"
"PATH=${homeDir}/.local/bin:${homeDir}/.bun/bin:${homeDir}/.nix-profile/bin:${homeDir}/.local/share/pnpm:${homeDir}/.local/share/fnm/current/bin:${homeDir}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
];
EnvironmentFile = "${instanceDir}/.env";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the .env file does not exist, systemd will fail to start the service. If this file is optional or manually created, you should prefix the path with - to allow the service to start without it.

      EnvironmentFile = "-${instanceDir}/.env";

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnvironmentFile will cause the service to fail to start if ${instanceDir}/.env does not exist (systemd treats missing EnvironmentFile as an error unless it’s prefixed with -). Consider either creating an empty .env during activation (with restrictive permissions) or using EnvironmentFile = "-${instanceDir}/.env"; so the service can start with defaults.

Suggested change
EnvironmentFile = "${instanceDir}/.env";
EnvironmentFile = "-${instanceDir}/.env";

Copilot uses AI. Check for mistakes.

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This service requires ${instanceDir}/.env, but this PR only provisions config.json. On a fresh setup, paperclip.service will fail before exec unless that env file already exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/paperclip/default.nix, line 41:

<comment>This service requires `${instanceDir}/.env`, but this PR only provisions `config.json`. On a fresh setup, `paperclip.service` will fail before exec unless that env file already exists.</comment>

<file context>
@@ -0,0 +1,50 @@
+        "HOME=${homeDir}"
+        "PATH=${homeDir}/.local/bin:${homeDir}/.bun/bin:${homeDir}/.nix-profile/bin:${homeDir}/.local/share/pnpm:${homeDir}/.local/share/fnm/current/bin:${homeDir}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
+      ];
+      EnvironmentFile = "${instanceDir}/.env";
+      WorkingDirectory = "${homeDir}/.paperclip";
+      StandardOutput = "append:/tmp/paperclip/paperclip.log";
</file context>
Fix with Cubic

WorkingDirectory = "${homeDir}/.paperclip";
StandardOutput = "append:/tmp/paperclip/paperclip.log";

@cubic-dev-ai cubic-dev-ai Bot Apr 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Logging to /tmp/paperclip/paperclip.log is unreliable — /tmp is cleared on reboot and the activation script that creates the directory doesn't run on every boot. The service will fail to start after reboot. Either use the systemd journal (remove these lines) or use LogsDirectory=paperclip to let systemd manage the directory lifecycle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/paperclip/default.nix, line 43:

<comment>Logging to `/tmp/paperclip/paperclip.log` is unreliable — `/tmp` is cleared on reboot and the activation script that creates the directory doesn't run on every boot. The service will fail to start after reboot. Either use the systemd journal (remove these lines) or use `LogsDirectory=paperclip` to let systemd manage the directory lifecycle.</comment>

<file context>
@@ -0,0 +1,50 @@
+      ];
+      EnvironmentFile = "${instanceDir}/.env";
+      WorkingDirectory = "${homeDir}/.paperclip";
+      StandardOutput = "append:/tmp/paperclip/paperclip.log";
+      StandardError = "append:/tmp/paperclip/paperclip.log";
+    };
</file context>
Fix with Cubic

StandardError = "append:/tmp/paperclip/paperclip.log";
Comment on lines +42 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Logging to /tmp is volatile and can cause the service to fail if the directory is missing after a reboot. It is recommended to use the systemd journal (the default) or a persistent path. Removing these lines will default logging to the journal, which is generally preferred for systemd services.

      WorkingDirectory = "${homeDir}/.paperclip";

};
Install = {
WantedBy = [ "default.target" ];
};
};
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"mcporter": "^0.8.1",
"open-composer": "^0.8.23",
"openclaw": "^2026.3.28",
"paperclipai": "^2026.325.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the paperclipai package exists and check its latest version
curl -s "https://registry.npmjs.org/paperclipai" | jq -r '.["dist-tags"].latest // "Package not found"'

Repository: shunkakinoki/dotfiles

Length of output: 75


🏁 Script executed:

cat package.json | sed -n '50,120p'

Repository: shunkakinoki/dotfiles

Length of output: 1763


Dependencies and trustedDependencies are not in alphabetical order.

paperclipai is placed between openclaw and oxfmt, breaking alphabetical order. Alphabetically, oxfmt should come before paperclipai in both the dependencies section and trustedDependencies array.

🔧 Suggested fix
     "openclaw": "^2026.3.28",
-    "paperclipai": "^2026.325.0",
     "oxfmt": "^0.42.0",
     "oxlint": "^1.57.0",
+    "paperclipai": "^2026.325.0",
     "portless": "^0.7.2",

Also reorder in trustedDependencies array:

     "openclaw",
-    "paperclipai",
     "oxfmt",
     "oxlint",
+    "paperclipai",
     "portless",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 59, The dependencies and trustedDependencies entries
are out of alphabetical order: move "paperclipai" so that "oxfmt" appears before
"paperclipai" in the dependencies object and apply the same ordering change
inside the trustedDependencies array; update the entries referencing "openclaw",
"oxfmt", and "paperclipai" to preserve alphabetical order in both the
dependencies and trustedDependencies sections.

"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"portless": "^0.7.2",
Expand Down Expand Up @@ -112,6 +113,7 @@
"mcporter",
"open-composer",
"openclaw",
"paperclipai",
"oxfmt",
"oxlint",
"portless",
Expand Down
5 changes: 5 additions & 0 deletions spec/coverage_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ It 'has spec file for config/openclaw/hydrate.sh'
The path "spec/openclaw_hydrate_spec.sh" should be exist
End

It 'has spec file for config/paperclip/setup.sh'
The path "spec/paperclip_setup_spec.sh" should be exist
End

It 'has spec file for home-manager/modules/npm-globals/install-npm-globals.sh'
The path "spec/npm_globals_spec.sh" should be exist
End
Expand Down Expand Up @@ -251,6 +255,7 @@ scripts/sync-rtk-rewrite.sh
config/hyprland/scripts/record-screen.sh
config/hyprland/scripts/toggle-terminal.sh
config/openclaw/hydrate.sh
config/paperclip/setup.sh
home-manager/modules/cargo-globals/install-cargo-globals.sh
home-manager/modules/local-binaries/sync-local-binaries.sh
home-manager/modules/local-scripts/clipboard-copy.sh
Expand Down
53 changes: 53 additions & 0 deletions spec/paperclip_setup_spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# shellcheck disable=SC2329

Describe 'config/paperclip/setup.sh'
SCRIPT="$PWD/config/paperclip/setup.sh"

Describe 'script properties'
It 'uses bash shebang'
When run bash -c "head -1 '$SCRIPT'"
The output should include '#!/usr/bin/env bash'
End

It 'uses strict mode'
When run bash -c "head -5 '$SCRIPT'"
The output should include 'set -euo pipefail'
End
End

Describe 'config management'
It 'creates instance directory'
When run bash -c "grep 'mkdir -p' '$SCRIPT'"
The output should include 'INSTANCE_DIR'
End

It 'copies config file to instance directory'
When run bash -c "grep '@cp@' '$SCRIPT'"
The output should include 'config_file'
End

It 'sets restrictive permissions on config'
When run bash -c "grep 'chmod 600' '$SCRIPT'"
The output should include 'CONFIG'
End
End

Describe 'database provisioning'
It 'creates paperclip database on docker-postgres'
When run bash -c "grep 'createdb' '$SCRIPT'"
The output should include 'paperclip'
End

It 'checks if database already exists before creating'
When run bash -c "grep 'grep -qw paperclip' '$SCRIPT'"
The output should include 'paperclip'
End

It 'only runs on kyber'
When run bash -c "grep 'is_kyber' '$SCRIPT'"
The output should include 'is_kyber'
End
End

End
Loading