patchcord 0.5.103 → 0.5.105

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.
package/bin/patchcord.mjs CHANGED
@@ -797,7 +797,7 @@ if (cmd === "subscribe") {
797
797
  // (docs/main-agent.md). The "main" is the managing agent of a team; its main
798
798
  // token is a user-scoped provisioning credential stored at ~/.patchcord/main.json
799
799
  // (legacy: master.json) or $PATCHCORD_MAIN_TOKEN (legacy: $PATCHCORD_MASTER_TOKEN).
800
- if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team") {
800
+ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
801
801
  const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
802
802
  const MAIN_CONFIG = join(HOME, ".patchcord", "main.json");
803
803
  const LEGACY_CONFIG = join(HOME, ".patchcord", "master.json"); // pre-rename
@@ -1134,6 +1134,73 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team")
1134
1134
  console.error("Usage: patchcord team <init|list|launch|status>");
1135
1135
  process.exit(1);
1136
1136
  }
1137
+
1138
+ if (cmd === "schedule") {
1139
+ // User-level scheduled / recurring messages (server Plan 041). Authed by
1140
+ // the main token — the user can manage schedules in any namespace they own.
1141
+ const m = requireMain();
1142
+ const sub = process.argv[3];
1143
+ const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
1144
+ const when = (s) => s.cron_expr ? `cron ${s.cron_expr}` : s.interval_sec ? `every ${s.interval_sec}s` : s.fire_at ? `once @ ${s.fire_at}` : s.schedule_kind;
1145
+ const fmt = (s) => `${s.id} ${M.green}${s.namespace_id}:${s.to_agent}${M.rst} ${s.active ? "" : M.dim + "[paused] " + M.rst}${s.name} ${M.dim}${when(s)}${s.next_fire_at ? ` → ${s.next_fire_at}` : ""}${M.rst}`;
1146
+
1147
+ if (sub === "list") {
1148
+ const ns = flagVal("namespace");
1149
+ const url = ns ? `${BASE}?namespace=${encodeURIComponent(ns)}` : BASE;
1150
+ const { status, json } = await _httpJSON("GET", url, m.token);
1151
+ if (status !== "200") { console.error(`list failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1152
+ const items = json?.schedules || [];
1153
+ for (const s of items) console.log(" " + fmt(s));
1154
+ if (!items.length) console.log(" (no schedules)");
1155
+ process.exit(0);
1156
+ }
1157
+ if (sub === "create") {
1158
+ const name = process.argv[4];
1159
+ if (!name || name.startsWith("-")) { console.error('Usage: patchcord schedule create <name> --namespace <ns> --to <agent> --content "..." (--at <ISO> | --cron "<expr>" | --every <sec>) [--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]'); process.exit(1); }
1160
+ const ns = flagVal("namespace"), to = flagVal("to"), content = flagVal("content");
1161
+ if (!ns || !to || !content) { console.error("--namespace, --to, and --content are required"); process.exit(1); }
1162
+ const at = flagVal("at"), cron = flagVal("cron"), every = flagVal("every");
1163
+ let schedule_kind, extra = {};
1164
+ if (cron) { schedule_kind = "cron"; extra.cron_expr = cron; }
1165
+ else if (every) { schedule_kind = "interval"; extra.interval_sec = parseInt(every, 10); }
1166
+ else if (at) { schedule_kind = "once"; extra.fire_at = at; }
1167
+ else { console.error('one of --at <ISO>, --cron "<expr>", or --every <seconds> is required'); process.exit(1); }
1168
+ const body = { namespace: ns, name, to_agent: to, content, schedule_kind, timezone: flagVal("timezone", "UTC"), ...extra };
1169
+ const thread = flagVal("thread"); if (thread) body.thread_slug = thread;
1170
+ const maxRuns = flagVal("max-runs"); if (maxRuns) body.max_runs = parseInt(maxRuns, 10);
1171
+ const expires = flagVal("expires"); if (expires) body.expires_at = expires;
1172
+ const { status, json } = await _httpJSON("POST", BASE, m.token, body);
1173
+ if (status !== "201" && status !== "200") { console.error(`create failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1174
+ console.log(`✓ scheduled ${M.green}${ns}:${to}${M.rst} [${schedule_kind}] ${M.dim}${json?.schedule?.id || ""}${M.rst}`);
1175
+ process.exit(0);
1176
+ }
1177
+ if (sub === "cancel" || sub === "delete" || sub === "rm") {
1178
+ const id = process.argv[4];
1179
+ if (!id) { console.error("Usage: patchcord schedule cancel <id>"); process.exit(1); }
1180
+ const { status, json } = await _httpJSON("DELETE", `${BASE}/${id}`, m.token);
1181
+ if (status !== "200") { console.error(`cancel failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1182
+ console.log(`✓ cancelled ${id}`);
1183
+ process.exit(0);
1184
+ }
1185
+ if (sub === "test" || sub === "fire") {
1186
+ const id = process.argv[4];
1187
+ if (!id) { console.error("Usage: patchcord schedule test <id>"); process.exit(1); }
1188
+ const { status, json } = await _httpJSON("POST", `${BASE}/${id}/test`, m.token, {});
1189
+ if (status !== "200") { console.error(`test failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1190
+ console.log(`✓ fired ${id} once (does not consume max-runs)`);
1191
+ process.exit(0);
1192
+ }
1193
+ if (sub === "pause" || sub === "resume") {
1194
+ const id = process.argv[4];
1195
+ if (!id) { console.error(`Usage: patchcord schedule ${sub} <id>`); process.exit(1); }
1196
+ const { status, json } = await _httpJSON("PATCH", `${BASE}/${id}`, m.token, { active: sub === "resume" });
1197
+ if (status !== "200") { console.error(`${sub} failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1198
+ console.log(`✓ ${sub === "resume" ? "resumed" : "paused"} ${id}`);
1199
+ process.exit(0);
1200
+ }
1201
+ console.error("Usage: patchcord schedule <create|list|cancel|test|pause|resume>");
1202
+ process.exit(1);
1203
+ }
1137
1204
  }
1138
1205
 
1139
1206
  if (cmd === "update" || cmd === "--update") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.103",
3
+ "version": "0.5.105",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: patchcord-inbox
3
3
  description: Read Patchcord inbox and reply to messages
4
- type: flow
5
4
  ---
6
5
 
7
- > Use `/flow:patchcord-inbox` to run this. `/skill:` only loads it as docs (Kimi design).
6
+ Run this with `/skill:patchcord-inbox`. (Kimi Code has no `/flow:` — `/skill:` injects this as a prompt and you act on it.)
8
7
 
9
- ```mermaid
10
- flowchart TD
11
- A([BEGIN]) --> B["Call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is on a 'From X' line — never confuse the header with the sender. For each pending message, classify it: (1) ACK — short signals like thanks, noted, works, great, ok, 👍 with no task. Close silently with reply(message_id, resolve=true) and NO content. Never send a text reply to an ack — it creates infinite ack chains. (2) BLOCKED — you cannot do the work right now (busy, missing credentials, ambiguous target). Use reply(message_id, reason, defer=true) — the message stays in your inbox as a reminder. Never silently skip a message. (3) ACTIONABLE — do the work the message asks for first (edit the file, run the command, write the code), THEN call reply(message_id, concrete summary) with file paths and line numbers. Never reply will-do or understood before doing the work."]
12
- B --> C["Call the Shell tool with these exact parameters to re-arm the listener: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener."]
13
- C --> D([END])
14
- ```
8
+ Call the `inbox` MCP tool now. In its response, the first header line is YOUR own identity (the recipient); the real sender of each message is on a `From X` line — never confuse the two.
9
+
10
+ For each pending message, classify it and act:
11
+ - **ACK** short signals like thanks, noted, works, great, ok, 👍 with no task close it silently: `reply(message_id, resolve=true)` with NO content. Never send a text reply to an ack (it creates infinite ack chains).
12
+ - **BLOCKED** — you cannot do the work right now (busy, missing credentials, ambiguous target) → `reply(message_id, "<reason>", defer=true)` so it stays in your inbox as a reminder. Never silently skip a message.
13
+ - **ACTIONABLE** — do the work the message asks for FIRST (edit the file, run the command, write the code), THEN `reply(message_id, "<concrete summary of what you did, with file paths and line numbers>")`. Never reply "will do" / "understood" before doing the work.
14
+
15
+ After handling every message, re-arm the background listener: use the Shell tool to run `patchcord subscribe --kimi 5` with run_in_background=true, timeout=86400, description="Patchcord listener".
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  name: patchcord-subscribe
3
3
  description: Start the Patchcord background polling listener
4
- type: flow
5
4
  ---
6
5
 
7
- > Use `/flow:patchcord-subscribe` to run this. `/skill:` only loads it as docs (Kimi design).
6
+ Run this with `/skill:patchcord-subscribe`. (Kimi Code has no `/flow:` — `/skill:` injects this as a prompt and you act on it.)
8
7
 
9
- ```mermaid
10
- flowchart TD
11
- A([BEGIN]) --> B["First, call the inbox MCP tool. Reading the response: the first header line shows YOUR own identity (the recipient). The actual sender of each message is on a 'From X' line — never confuse the header with the sender. Drain each pending message by classifying it: ACK (thanks, noted, works, ok, 👍) → close silently with reply(message_id, resolve=true), no content; BLOCKED (you cannot do the work right now) → reply(message_id, reason, defer=true); ACTIONABLE → do the work first, then reply(message_id, concrete summary). Do NOT skip the drain — the listener exits immediately if messages are already waiting."]
12
- B --> C["Now call the Shell tool with these exact parameters: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener. Then tell the user in one sentence: Patchcord listener active."]
13
- C --> D([END])
14
- ```
8
+ First, call the `inbox` MCP tool and DRAIN every pending message before starting the listener (the listener exits immediately if messages are already waiting). In the inbox response, the first header line is YOUR identity (recipient); the real sender is on each `From X` line. For each message:
9
+ - **ACK** (thanks, noted, works, ok, 👍) → `reply(message_id, resolve=true)`, no content.
10
+ - **BLOCKED**`reply(message_id, "<reason>", defer=true)`.
11
+ - **ACTIONABLE** do the work first, then `reply(message_id, "<what you did>")`.
12
+
13
+ Then start the background listener: use the Shell tool to run `patchcord subscribe --kimi 5` with run_in_background=true, timeout=86400, description="Patchcord listener". Tell the user in one sentence: Patchcord listener active.
@@ -1,17 +1,15 @@
1
1
  ---
2
2
  name: patchcord-wait
3
3
  description: Wait for one incoming Patchcord message
4
- type: flow
5
4
  ---
6
5
 
7
- > Use `/flow:patchcord-wait` to run this. `/skill:` only loads it as docs (Kimi design).
6
+ Run this with `/skill:patchcord-wait`. (Kimi Code has no `/flow:` — `/skill:` injects this as a prompt and you act on it.)
8
7
 
9
- ```mermaid
10
- flowchart TD
11
- A([BEGIN]) --> B["Call the wait_for_message MCP tool to block until a message arrives or 5 minutes elapse."]
12
- B --> C{"Message arrived?"}
13
- C -->|Yes| D["Classify the message: ACK (thanks, noted, works, ok, 👍) → close silently with reply(message_id, resolve=true), no content; BLOCKED (cannot do work right now) → reply(message_id, reason, defer=true); ACTIONABLE → do the work first, then reply(message_id, concrete summary). Never reply to an ack with text."]
14
- C -->|No| E([END])
15
- D --> F["Call the Shell tool with these exact parameters to re-arm the listener: command=patchcord subscribe --kimi 5, run_in_background=true, timeout=86400, description=Patchcord listener."]
16
- F --> E
17
- ```
8
+ Call the `wait_for_message` MCP tool now to block until a message arrives or ~5 minutes elapse.
9
+
10
+ When a message arrives, classify it and act:
11
+ - **ACK** (thanks, noted, works, ok, 👍, no task) → `reply(message_id, resolve=true)` with NO content. Never text-reply an ack.
12
+ - **BLOCKED** (cannot do the work right now) → `reply(message_id, "<reason>", defer=true)`.
13
+ - **ACTIONABLE** → do the work FIRST, then `reply(message_id, "<concrete summary of what you did>")`.
14
+
15
+ Then re-arm the background listener: use the Shell tool to run `patchcord subscribe --kimi 5` with run_in_background=true, timeout=86400, description="Patchcord listener".