Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "world2agent-plugins",
"owner": {
"name": "MachinePulse AI"
"name": "MachinePulse Pte. Ltd."
},
"metadata": {
"description": "World2Agent plugins for Claude Code — give your agent real-time awareness of external events via pluggable sensors."
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2026 MachinePulse AI
Copyright 2026 MachinePulse Pte. Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
4 changes: 2 additions & 2 deletions claude-code-channel/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "world2agent",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"description": "Give Claude Code real-time awareness of external events via World2Agent sensors (Hacker News, GitHub, stocks, X, calendars, and more)",
"author": { "name": "MachinePulse AI" },
"author": { "name": "MachinePulse Pte. Ltd." },
"homepage": "https://github.com/machinepulse-ai/world2agent",
"repository": "https://github.com/machinepulse-ai/world2agent-plugins",
"license": "Apache-2.0",
Expand Down
20 changes: 13 additions & 7 deletions claude-code-channel/commands/sensor-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,21 @@ Fill the skill template from SETUP.md with the user's answers and write it to th

If no project directory exists or the user prefers global, write to `~/.claude/skills/<skill_id>/SKILL.md` instead.

## 5. Hot-reload the new sensor
## 5. Tell the user to start a new session

Call the `reload_sensors` MCP tool exposed by the world2agent channel. It re-reads `~/.world2agent/config.json`, imports any newly-added packages, and starts them in the already-running MCP process — no session restart needed.
A freshly-installed sensor is **not** picked up by the running MCP process — Node's module resolution caches `node_modules` and doesn't see the new package without a process restart. So after the config file + handler skill are written, the sensor still won't run until the user exits this session and starts a new one.

After the tool returns, tell the user what happened (which sensor started, or any error from the tool's text response).
Tell the user, in their language, to:

**Restart is only required when:**
1. Quit this Claude Code session.
2. Run:

- `reload_sensors` reports that a sensor failed to load because the package can't be resolved. In that case the user needs to exit and re-run `claude --dangerously-load-development-channels plugin:world2agent@world2agent-plugins` so Node picks up freshly-installed `node_modules`.
- The user wants to *remove* a sensor. `reload_sensors` only starts newly-added sensors; it does not stop removed ones.
```bash
claude --dangerously-load-development-channels plugin:world2agent@world2agent-plugins
```

`/reload-plugins` alone is NOT sufficient for either case — it doesn't touch the MCP process.
3. The new sensor will start automatically once the new session boots.

`/reload-plugins` alone is NOT sufficient — it reloads plugin definitions but does not refresh Node's `node_modules` view inside the running MCP channel.

`reload_sensors` (the channel's MCP tool) is for *config-only* changes after the package is already imported — e.g. you edit `~/.world2agent/config.json` to tweak a parameter or remove a sensor. **It does not reliably load brand-new packages**, so don't use it as a substitute for the restart in this install flow.
130 changes: 118 additions & 12 deletions claude-code-channel/dist/bin.bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11180,7 +11180,7 @@ function configFromEnv2(slug) {
}

// src/bin.ts
import { existsSync as existsSync3 } from "node:fs";
import { existsSync as existsSync3, appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
import { dirname as dirname2, join as join3 } from "node:path";
import { createRequire } from "node:module";
import { homedir as homedir3 } from "node:os";
Expand Down Expand Up @@ -18352,6 +18352,20 @@ function stableStringify(value) {
}
async function main() {
console.error("[world2agent] Starting World2Agent channel for Claude Code...");
const channelLogPath = join3(homedir3(), ".world2agent", "channel.log");
const log = (msg) => {
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [world2agent] ${msg}`;
console.error(line);
try {
appendFileSync(channelLogPath, line + "\n");
} catch {
try {
mkdirSync2(dirname2(channelLogPath), { recursive: true });
appendFileSync(channelLogPath, line + "\n");
} catch {
}
}
};
const bootConfig = loadConfig();
const noSensorsConfigured = bootConfig.sensors.length === 0;
if (noSensorsConfigured) {
Expand Down Expand Up @@ -18402,13 +18416,21 @@ async function main() {
notificationGate.then(() => {
gateOpen = true;
});
let channelEnabled = null;
let resolveChannelReady = () => {
};
const channelReady = new Promise((resolve) => {
resolveChannelReady = resolve;
});
channelReady.then((v) => {
channelEnabled = v;
});
mcp.oninitialized = () => {
console.error("[world2agent] client initialized \u2014 opening notification gate");
log("Client initialized");
openGate();
};
const sendNotification = async (params) => {
if (!gateOpen) {
console.error(`[world2agent] notification queued until client init: ${params.method}`);
await notificationGate;
}
await mcp.notification(params);
Expand Down Expand Up @@ -18553,7 +18575,20 @@ ${body}`;
return parts.join(" ");
};
let initialStartDone = !needsSetup;
const channelDisabledMessage = () => "Channel notifications are not flowing in this Claude Code session (the boot handshake was never acknowledged), so sensors were NOT started. Polling now would write seen-id state to ~/.world2agent/state.json and pollute the dedup cache \u2014 when you next launch Claude Code with `--dangerously-load-development-channels plugin:world2agent@world2agent-plugins`, the sensor would silently swallow the first real batch of signals.\n\nWhat to do: exit this session and relaunch Claude Code with that flag. Sensor config in ~/.world2agent/config.json is already saved; no need to re-run /world2agent:sensor-add.\n\nSee ~/.world2agent/channel.log for the boot trace.";
const confirmChannelReceivedTool = async () => {
if (channelEnabled === true) {
return "Channel handshake already confirmed \u2014 no action needed.";
}
if (channelEnabled === false) {
return "Channel was previously marked disabled (handshake timed out). Restart Claude Code to retry.";
}
log("Channel handshake ack received \u2014 channel notifications are flowing.");
resolveChannelReady(true);
return "Channel handshake confirmed. Proceeding with sensor startup.";
};
const startSensorsTool = async () => {
if (!await channelReady) return channelDisabledMessage();
if (initialStartDone) {
const diff2 = await applyConfig(loadConfig().sensors, { gateOnHandlerSkill: false });
return summarizeDiff(diff2);
Expand All @@ -18563,13 +18598,23 @@ ${body}`;
return summarizeDiff(diff);
};
const reloadSensorsTool = async () => {
console.error("[world2agent] reload_sensors: re-reading config...");
if (!await channelReady) return channelDisabledMessage();
log("reload_sensors: re-reading config...");
const fresh = loadConfig();
const diff = await applyConfig(fresh.sensors, { gateOnHandlerSkill: true });
return summarizeDiff(diff);
};
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "confirm_channel_received",
description: "Acknowledge that this Claude Code session is receiving World2Agent channel notifications. Call this IMMEDIATELY when you see a channel message asking you to do so \u2014 sensors will not start until you do.",
inputSchema: {
type: "object",
properties: {},
required: []
}
},
...needsSetup ? [{
name: "start_sensors",
description: "Start the signal sensors. Call this after the user has configured their preferences, or if they choose to skip configuration.",
Expand All @@ -18591,6 +18636,10 @@ ${body}`;
]
}));
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === "confirm_channel_received") {
const result = await confirmChannelReceivedTool();
return { content: [{ type: "text", text: result }] };
}
if (req.params.name === "start_sensors") {
const result = await startSensorsTool();
return { content: [{ type: "text", text: result }] };
Expand All @@ -18611,11 +18660,53 @@ ${body}`;
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
console.error(`[world2agent] Connecting to Claude Code...`);
process.stdin.on("end", () => {
console.error("[world2agent] stdin ended \u2014 shutting down");
void shutdown();
});
process.stdin.on("close", () => {
console.error("[world2agent] stdin closed \u2014 shutting down");
void shutdown();
});
process.stdin.on("error", (err) => {
console.error("[world2agent] stdin error \u2014 shutting down:", err);
void shutdown();
});
log(`Connecting to Claude Code...`);
await mcp.connect(new StdioServerTransport());
console.error(`[world2agent] Connected. Waiting for client init before sending notifications...`);
if (noSensorsConfigured) {
console.error(`[world2agent] Sending onboarding notification.`);
log(`Connected. Sending channel handshake (will queue until client init)...`);
if (channelEnabled === null) {
const HANDSHAKE_DELAY_MS = 5e3;
setTimeout(() => {
log(`Firing handshake notification (${HANDSHAKE_DELAY_MS}ms post-connect)...`);
void sendNotification({
method: "notifications/claude/channel",
params: {
content: "World2Agent channel handshake.\n\nIf you are reading this message, channel notifications are flowing in this Claude Code session. Please IMMEDIATELY call the `confirm_channel_received` tool with no arguments \u2014 World2Agent sensors will not start polling until you do.\n\nDon't reply to the user about this message; just call the tool. After acknowledgment, normal sensor signals will follow as usual.",
meta: {
event_type: "system.world2agent.handshake",
reason: "boot_handshake"
}
}
}).catch((err) => log(`Handshake notify error: ${err}`));
}, HANDSHAKE_DELAY_MS);
const HANDSHAKE_TIMEOUT_MS = 3e4;
setTimeout(() => {
if (channelEnabled === null) {
log(
`Channel handshake not acknowledged within ${HANDSHAKE_TIMEOUT_MS}ms \u2014 treating as disabled.`
);
resolveChannelReady(false);
}
}, HANDSHAKE_TIMEOUT_MS);
}
const clientChannelEnabled = await channelReady;
if (!clientChannelEnabled) {
log(
"Channel disabled (handshake not acknowledged). Skipping sensor startup to avoid polluting ~/.world2agent/state.json. Tools (start_sensors / reload_sensors) will return a help message if called. To enable, relaunch Claude Code with `--dangerously-load-development-channels plugin:world2agent@world2agent-plugins`."
);
} else if (noSensorsConfigured) {
log(`Sending onboarding notification.`);
await sendNotification({
method: "notifications/claude/channel",
params: {
Expand All @@ -18627,9 +18718,9 @@ ${body}`;
}
});
} else if (needsSetup) {
console.error(`[world2agent] Waiting for start_sensors tool call...`);
log(`Waiting for start_sensors tool call...`);
const sensorList = sensorsNeedingSetup.map((s) => s.skillId).join(", ");
console.error(`[world2agent] Sending setup prompt notification for: ${sensorList}`);
log(`Sending setup prompt notification for: ${sensorList}`);
await sendNotification({
method: "notifications/claude/channel",
params: {
Expand All @@ -18641,9 +18732,9 @@ ${body}`;
}
});
} else {
console.error(`[world2agent] Starting ${bootEnabled.length} sensor(s)...`);
log(`Starting ${bootEnabled.length} sensor(s)...`);
const diff = await applyConfig(bootEnabled, { gateOnHandlerSkill: false });
console.error(`[world2agent] ${summarizeDiff(diff)}`);
log(summarizeDiff(diff));
if (diff.failed.length > 0 && handles.size === 0) {
await sendNotification({
method: "notifications/claude/channel",
Expand All @@ -18655,6 +18746,21 @@ ${body}`;
}
}
});
} else if (diff.started.length > 0) {
const running = diff.started.join(", ");
log(`Sending ready notification.`);
await sendNotification({
method: "notifications/claude/channel",
params: {
content: `World2Agent is now active and listening for signals from ${diff.started.length} sensor(s): ${running}.

In one short sentence, tell the user that World2Agent is ready and which source(s) it's watching, then return control to whatever the user was doing. Do not list installation steps or repeat this message.`,
meta: {
event_type: "system.world2agent.ready",
sensor_count: String(diff.started.length)
}
}
});
}
}
await new Promise(() => {
Expand Down
Loading
Loading