Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
45 changes: 32 additions & 13 deletions bridges/kimaki.sh
Original file line number Diff line number Diff line change
Expand Up @@ -811,8 +811,8 @@ _kimaki_install_systemd() {
NODE_BIN_DIR=$(_resolve_node_bin_dir "$KIMAKI_BIN")
PATH_VALUE=$(_compose_path_value "$KIMAKI_BIN_DIR" "$NODE_BIN_DIR" /usr/local/bin /usr/bin /bin)
_kimaki_assert_bin_identity "$KIMAKI_BIN" "$PATH_VALUE"
local DATAMACHINE_WP_CMD
DATAMACHINE_WP_CMD=$(_kimaki_datamachine_wp_cmd)
local DATAMACHINE_WP_TRANSPORT_JSON
DATAMACHINE_WP_TRANSPORT_JSON=$(_kimaki_datamachine_wp_transport_json_systemd)

# Kimaki recreates a general-purpose #kimaki-<bot> channel, welcome message,
# and tutorial thread on every start. On a wp-coding-agents install the real
Expand All @@ -824,7 +824,7 @@ _kimaki_install_systemd() {
Environment=PATH=$PATH_VALUE
Environment=KIMAKI_DATA_DIR=$KIMAKI_DATA_DIR
Environment=DATAMACHINE_SITE_PATH=$SITE_PATH
Environment=DATAMACHINE_WP_CMD=$DATAMACHINE_WP_CMD
Environment=DATAMACHINE_WP_TRANSPORT_JSON=$DATAMACHINE_WP_TRANSPORT_JSON
Environment=KIMAKI_NO_DEFAULT_CHANNEL=1"
if [ -n "${AGENT_SLUG:-}" ]; then
ENV_BLOCK="$ENV_BLOCK
Expand Down Expand Up @@ -1150,6 +1150,7 @@ bridge_update_systemd() {

local CURRENT_ENV
CURRENT_ENV=$(grep '^Environment=' "$UNIT_FILE" || true)
CURRENT_ENV=$(_kimaki_remove_systemd_env_key "$CURRENT_ENV" DATAMACHINE_WP_CMD)
if [ "${KIMAKI_DATA_DIR_EXPLICIT:-false}" = true ]; then
CURRENT_ENV=$(_kimaki_remove_systemd_env_key "$CURRENT_ENV" KIMAKI_DATA_DIR)
fi
Expand Down Expand Up @@ -1177,7 +1178,7 @@ bridge_update_systemd() {
Environment=PATH=$PATH_VALUE
Environment=KIMAKI_DATA_DIR=$KIMAKI_DATA_DIR
Environment=DATAMACHINE_SITE_PATH=$SITE_PATH
Environment=DATAMACHINE_WP_CMD=$(_kimaki_datamachine_wp_cmd)
Environment=DATAMACHINE_WP_TRANSPORT_JSON=$(_kimaki_datamachine_wp_transport_json_systemd)
Environment=KIMAKI_NO_DEFAULT_CHANNEL=1"
if [ -n "${KIMAKI_LOCK_PORT:-}" ]; then
TEMPLATE_ENV="$TEMPLATE_ENV
Expand Down Expand Up @@ -1323,8 +1324,8 @@ bridge_render_launchd() {
skill_filter_plist_args="$(_kimaki_skill_filter_args_plist)"
local launchd_start
launchd_start="${KIMAKI_DATA_DIR}/kimaki-config/launchd-start.sh"
local datamachine_wp_cmd
datamachine_wp_cmd=$(_kimaki_datamachine_wp_cmd)
local datamachine_wp_transport_json
datamachine_wp_transport_json=$(_kimaki_datamachine_wp_transport_json_xml)
cat <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
Expand Down Expand Up @@ -1360,8 +1361,8 @@ $skill_filter_plist_args
<string>${KIMAKI_DATA_DIR}/kimaki-config</string>
<key>DATAMACHINE_SITE_PATH</key>
<string>$SITE_PATH</string>
<key>DATAMACHINE_WP_CMD</key>
<string>$datamachine_wp_cmd</string>
<key>DATAMACHINE_WP_TRANSPORT_JSON</key>
<string>$datamachine_wp_transport_json</string>
<key>KIMAKI_NO_DEFAULT_CHANNEL</key>
<string>1</string>$(if [ -n "${AGENT_SLUG:-}" ]; then echo "
<key>DATAMACHINE_AGENT_SLUG</key>
Expand All @@ -1374,14 +1375,32 @@ $skill_filter_plist_args
EOF
}

_kimaki_datamachine_wp_cmd() {
_kimaki_datamachine_wp_transport_json() {
if [ "${EXTERNAL_WORDPRESS:-false}" = true ]; then
external_wordpress_control_command
python3 - "$(external_wordpress_control_command)" <<'PY'
import json, sys
print(json.dumps(sys.argv[1:], separators=(",", ":")))
PY
return 0
fi
wp_cli_transport_ensure
wp_cli_transport_display
printf '\n'
wp_cli_transport_json
}

_kimaki_datamachine_wp_transport_json_systemd() {
local value
value=$(_kimaki_datamachine_wp_transport_json)
value=${value//\\/\\\\}
value=${value//\"/\\\"}
printf '%s\n' "$value"
}

_kimaki_datamachine_wp_transport_json_xml() {
local value
value=$(_kimaki_datamachine_wp_transport_json)
value=${value//&/\&amp;}
value=${value//</\&lt;}
value=${value//>/\&gt;}
printf '%s\n' "$value"
}

_kimaki_skill_filter_mode() {
Expand Down
41 changes: 31 additions & 10 deletions bridges/kimaki/plugins/dm-agent-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { spawn } from "node:child_process";
import type { Plugin } from "@opencode-ai/plugin";

type WpCli = string;
type WpCli = string[];

const DEFAULT_COMPOSE_TIMEOUT_MS = 10_000;
const OUTPUT_LIMIT = 16 * 1024;
Expand All @@ -25,8 +25,12 @@ const dmAgentSync: Plugin = async () => {

// Config runs for every CLI command. Capture only local state here;
// invoking WordPress belongs to the real chat lifecycle below.
const wpCli = resolveWpCliTransport();
if (!wpCli) {
return;
}
sessionConfig = {
wpCli: process.env.DATAMACHINE_WP_CMD || process.env.WP_CMD || "wp",
wpCli,
sitePath: getSitePath(),
agentSlug: getAgentSlug(input),
};
Expand All @@ -48,7 +52,7 @@ const dmAgentSync: Plugin = async () => {

async function composeMemory(wpCli: WpCli, sitePath: string, agentSlug: string): Promise<void> {
const startedAt = Date.now();
const result = await runBoundedCommand(datamachineCommand(wpCli, sitePath, agentSlug), getComposeTimeoutMs());
const result = await runBoundedCommand(datamachineArgv(wpCli, sitePath, agentSlug), getComposeTimeoutMs());
const durationMs = Date.now() - startedAt;

if (result.timedOut) {
Expand All @@ -67,21 +71,21 @@ async function composeMemory(wpCli: WpCli, sitePath: string, agentSlug: string):
console.warn(`[dm-agent-sync] recomposed Data Machine memory in ${durationMs}ms`);
}

function datamachineCommand(wpCli: WpCli, sitePath: string, agentSlug: string): string {
const args = ["datamachine", "memory", "compose"];
function datamachineArgv(wpCli: WpCli, sitePath: string, agentSlug: string): string[] {
const args = [...wpCli, "datamachine", "memory", "compose"];
if (agentSlug) {
args.push(`--agent=${agentSlug}`);
}
if (sitePath) {
args.push(`--path=${sitePath}`);
}
args.push("--allow-root");
return [wpCli, ...args.map(shellQuote)].join(" ");
return args;
}

function runBoundedCommand(command: string, timeoutMs: number): Promise<{ exitCode: number; output: string; timedOut: boolean }> {
function runBoundedCommand(argv: string[], timeoutMs: number): Promise<{ exitCode: number; output: string; timedOut: boolean }> {
return new Promise((resolve) => {
const child = spawn("sh", ["-c", command], { detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
const child = spawn(argv[0], argv.slice(1), { detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
let output = "";
let settled = false;
const finish = (exitCode: number, timedOut: boolean) => {
Expand Down Expand Up @@ -145,8 +149,25 @@ function getAgentSlug(input: { instructions?: string[] }): string {
return "";
}

function shellQuote(value: string): string {
return `'${value.replace(/'/g, `"'"'`)}'`;
function resolveWpCliTransport(): WpCli | undefined {
const json = process.env.DATAMACHINE_WP_TRANSPORT_JSON;
if (json) {
try {
const value = JSON.parse(json) as unknown;
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0 && !item.includes("\0"))) {
return value;
}
} catch {
return undefined;
}
return undefined;
}

// Shipped service files used a whitespace-delimited command string. Keep
// that input boundary until upgraded services restart with canonical JSON.
const legacy = process.env.DATAMACHINE_WP_CMD || process.env.WP_CMD || "wp";
const parts = legacy.trim().split(/[ \t]+/).filter(Boolean);
return parts.length > 0 ? parts : undefined;
}

export default dmAgentSync;
4 changes: 2 additions & 2 deletions tests/__snapshots__/bridges/kimaki-launchd
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
<string>/home/chubes/.kimaki/kimaki-config</string>
<key>DATAMACHINE_SITE_PATH</key>
<string>/var/www/site</string>
<key>DATAMACHINE_WP_CMD</key>
<string>wp</string>
<key>DATAMACHINE_WP_TRANSPORT_JSON</key>
<string>["wp"]</string>
<key>KIMAKI_NO_DEFAULT_CHANNEL</key>
<string>1</string>
<key>DATAMACHINE_AGENT_SLUG</key>
Expand Down
2 changes: 1 addition & 1 deletion tests/__snapshots__/bridges/kimaki-systemd
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Environment=HOME=/home/chubes
Environment=PATH=/usr/bin:/usr/local/bin:/bin
Environment=KIMAKI_DATA_DIR=/home/chubes/.kimaki
Environment=DATAMACHINE_SITE_PATH=/var/www/site
Environment=DATAMACHINE_WP_CMD=wp
Environment=DATAMACHINE_WP_TRANSPORT_JSON=["wp"]
Environment=DATAMACHINE_AGENT_SLUG=intelligence-chubes4
# Reap stray opencode-serve children left behind by the previous kimaki
# process before starting a fresh one. Each kimaki session spawns its own
Expand Down
4 changes: 3 additions & 1 deletion tests/bridge-render.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ kimaki_env_block() {
kimaki_bin_dir=$(dirname "$KIMAKI_BIN")
node_bin_dir=$(_resolve_node_bin_dir "$KIMAKI_BIN")
path_value=$(_compose_path_value "$kimaki_bin_dir" "$node_bin_dir" /usr/local/bin /usr/bin /bin)
local wp_transport_json
printf -v wp_transport_json '[\"%s\"]' "$WP_CMD"
local out="Environment=HOME=$SERVICE_HOME
Environment=PATH=$path_value
Environment=KIMAKI_DATA_DIR=$KIMAKI_DATA_DIR
Environment=DATAMACHINE_SITE_PATH=$SITE_PATH
Environment=DATAMACHINE_WP_CMD=$WP_CMD
Environment=DATAMACHINE_WP_TRANSPORT_JSON=$wp_transport_json
Environment=DATAMACHINE_AGENT_SLUG=$AGENT_SLUG"
if [ -n "${KIMAKI_BOT_TOKEN:-}" ]; then
out="$out
Expand Down
47 changes: 42 additions & 5 deletions tests/dm-agent-sync.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// tests/dm-agent-sync.mjs — lifecycle tests for the Kimaki DM memory sync plugin.

import assert from "node:assert/strict"
import { access, mkdtemp } from "node:fs/promises"
import { access, chmod, mkdtemp, readFile, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import dmAgentSync from "../bridges/kimaki/plugins/dm-agent-sync.ts"
Expand Down Expand Up @@ -52,7 +52,8 @@ async function loadPlugin(config = {}) {

await withEnv({
DATAMACHINE_SITE_PATH: sitePath,
DATAMACHINE_WP_CMD: "true",
DATAMACHINE_WP_TRANSPORT_JSON: '["true"]',
DATAMACHINE_WP_CMD: undefined,
DATAMACHINE_AGENT_SLUG: "intelligence-chubes4",
EXTERNAL_WORDPRESS: undefined,
}, async () => {
Expand All @@ -74,7 +75,8 @@ await withEnv({

await withEnv({
DATAMACHINE_SITE_PATH: sitePath,
DATAMACHINE_WP_CMD: "false",
DATAMACHINE_WP_TRANSPORT_JSON: '["false"]',
DATAMACHINE_WP_CMD: undefined,
DATAMACHINE_AGENT_SLUG: "intelligence-chubes4",
}, async () => {
const run = await loadPlugin()
Expand All @@ -83,7 +85,7 @@ await withEnv({
assert.ok(run.warnings.some((line) => line.includes("memory compose failed")))
})

await withEnv({ EXTERNAL_WORDPRESS: "true", DATAMACHINE_WP_CMD: "false" }, async () => {
await withEnv({ EXTERNAL_WORDPRESS: "true", DATAMACHINE_WP_TRANSPORT_JSON: '["false"]' }, async () => {
const run = await loadPlugin()
await run.config()
await run.chat()
Expand All @@ -94,7 +96,7 @@ await withEnv({ DATAMACHINE_COMPOSE_TIMEOUT_MS: "10" }, async () => {
const directory = await mkdtemp(join(tmpdir(), "dm-agent-sync-"))
const marker = join(directory, "compose-finished")
const run = await loadPlugin()
await withEnv({ DATAMACHINE_WP_CMD: `sh -c 'sleep 0.2; touch ${marker}'` }, async () => {
await withEnv({ DATAMACHINE_WP_TRANSPORT_JSON: JSON.stringify(["sh", "-c", `sleep 0.2; touch ${marker}`]) }, async () => {
await run.config()
const startedAt = Date.now()
await run.chat()
Expand All @@ -105,4 +107,39 @@ await withEnv({ DATAMACHINE_COMPOSE_TIMEOUT_MS: "10" }, async () => {
assert.ok(run.warnings.some((line) => line.includes("memory compose timed out")))
})

await withEnv({ DATAMACHINE_WP_CMD: "true", DATAMACHINE_WP_TRANSPORT_JSON: undefined }, async () => {
const run = await loadPlugin()
await run.config()
await run.chat()
assert.ok(run.warnings.some((line) => line.includes("recomposed Data Machine memory in")))
})

await withEnv({ DATAMACHINE_SITE_PATH: sitePath, DATAMACHINE_WP_CMD: undefined }, async () => {
const directory = await mkdtemp(join(tmpdir(), "dm agent sync spaces "))
const executable = join(directory, "wp cli")
const output = join(directory, "received args")
await writeFile(executable, `#!/bin/sh\nprintf '%s\\n' "$@" > "$DM_SYNC_TEST_OUTPUT"\n`)
await chmod(executable, 0o755)

await withEnv({
DATAMACHINE_WP_TRANSPORT_JSON: JSON.stringify([executable, "fixed argument with spaces"]),
DM_SYNC_TEST_OUTPUT: output,
}, async () => {
const run = await loadPlugin()
await run.config()
await run.chat()
assert.ok(run.warnings.some((line) => line.includes("recomposed Data Machine memory in")))
})

const args = (await readFile(output, "utf8")).trim().split("\n")
assert.deepEqual(args, [
"fixed argument with spaces",
"datamachine",
"memory",
"compose",
`--path=${sitePath}`,
"--allow-root",
])
})

console.log("OK: dm-agent-sync runs bounded WordPress composition only for the first chat message per session")
9 changes: 9 additions & 0 deletions tests/kimaki-no-default-channel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,20 @@ DRY_RUN=false
TIMESTAMP="test"
UPDATED_ITEMS=()
WP_CMD=wp
WP_CLI_TRANSPORT=(wp)
IS_STUDIO=false
systemctl() { :; }

bridge_update_systemd

grep -q '^Environment=DATAMACHINE_WP_TRANSPORT_JSON=\[\\"wp\\"\]$' "$SYSTEMD_UNIT_DIR/kimaki.service"
check $? "upgrade writes the argv-native WordPress transport"
if grep -q '^Environment=DATAMACHINE_WP_CMD=' "$SYSTEMD_UNIT_DIR/kimaki.service"; then
check 1 "upgrade removes the legacy WordPress command"
else
check 0 "upgrade removes the legacy WordPress command"
fi

UNIT="$SYSTEMD_UNIT_DIR/kimaki.service"
[ "$(grep -c '^Environment=KIMAKI_NO_DEFAULT_CHANNEL=1$' "$UNIT")" -eq 1 ]
check $? "upgrade adds the opt-out exactly once"
Expand Down
Loading