moshcode 0.48.0 → 0.50.0

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/README.md CHANGED
@@ -79,8 +79,13 @@ moshcode install codex # npm i -g @openai/codex
79
79
  moshcode install kimi # curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
80
80
  moshcode install qwen # npm i -g @qwen-code/qwen-code
81
81
  moshcode install deepseek # npm i -g @serjm/deepseek-code
82
+ moshcode install openagents # curl -fsSL https://openagents.org/install.sh | bash
82
83
  ```
83
84
 
85
+ `openagents` is the odd one out: a launcher that supervises the engines above
86
+ rather than a coding agent itself. Its installer ends by offering to pair the
87
+ machine with a workspace — press Enter to skip that.
88
+
84
89
  ### Autonomous agents versus raw starts
85
90
 
86
91
  `agents` opens the engine's native **agent view** when it has one, so you land on
@@ -98,6 +103,7 @@ moshcode agents kimi # kimi --yolo
98
103
  moshcode agents qwen # qwen --approval-mode=yolo (autonomous)
99
104
  moshcode agents deepseek # deepseek-code --turbo (autonomous)
100
105
  moshcode agents aider # aider --yes-always (autonomous)
106
+ moshcode agents openagents # openagents (dashboard)
101
107
  ```
102
108
 
103
109
  `start` is the explicit raw path. It injects nothing, so the native engine keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
package/src/engines.mjs CHANGED
@@ -166,6 +166,38 @@ export const ENGINES = {
166
166
  agentArgs: ["--turbo"],
167
167
  install: { cmd: "npm", args: ["install", "-g", "@serjm/deepseek-code"] },
168
168
  },
169
+ openagents: {
170
+ desc: "OpenAgents — multi-agent launcher + dashboard (openagents.org)",
171
+ // The package installs three identical bins — `agn`, `openagents`, and
172
+ // `agent-connector`. Take the descriptive one: `agn` is short enough to
173
+ // collide with something else on a machine we don't control.
174
+ bin: "openagents",
175
+ // Not a coding agent of its own: it launches and supervises the engines
176
+ // above, so it has no approvals to bypass and no yolo flag to pass. A bare
177
+ // launch opens the interactive dashboard, which IS its agent list, so both
178
+ // `/agents openagents` and `moshcode start openagents` land in the same
179
+ // place. (No `agentsView` for the same reason — it would spell the same
180
+ // empty argv twice.)
181
+ agentArgs: [],
182
+ install: { cmd: "bash", args: ["-c", "curl -fsSL https://openagents.org/install.sh | bash"] },
183
+ // Its own updater ("upgrade launcher to the latest npm release"), which
184
+ // knows it was installed as an unpacked tarball under ~/.openagents rather
185
+ // than by npm -g. Re-running the installer would work too, but it re-does
186
+ // the Node bootstrap and ends on the pairing prompt below.
187
+ upgrade: { cmd: "openagents", args: ["update"] },
188
+ // The installer unpacks to ~/.openagents/nodejs and writes the bin shims
189
+ // there, appending that directory to a shell rc — so PATH will not see
190
+ // `openagents` until the next shell, including in the moshcode session that
191
+ // just installed it.
192
+ binDirs: [path.join(homedir(), ".openagents", "nodejs", "node_modules", ".bin")],
193
+ // No `state`: the dashboard is a blessed TUI with no wording of its own we
194
+ // have verified, and a guessed pattern is worse than the shared ones.
195
+ //
196
+ // The installer ends by offering to pair this device with a workspace, and
197
+ // waits on a real answer — but only when stderr is a terminal, and Enter
198
+ // skips it. `moshcode install` inherits the terminal, so that prompt is
199
+ // yours to answer; run it where you can reach a keyboard.
200
+ },
169
201
  aider: {
170
202
  desc: "Aider — pair-programming in your terminal",
171
203
  bin: "aider",
@@ -198,6 +230,9 @@ export const ENGINE_ALIASES = {
198
230
  // `dsc` and `deepseek-code` are the binary names the package installs; accept
199
231
  // both as engine names so whichever one someone has seen resolves.
200
232
  ds: "deepseek", dsc: "deepseek", "deepseek-code": "deepseek",
233
+ // Same idea for openagents: `agn` and `agent-connector` are the other two
234
+ // binary names its package installs, and the domain is how it's advertised.
235
+ oa: "openagents", agn: "openagents", "agent-connector": "openagents", "openagents.org": "openagents",
201
236
  };
202
237
 
203
238
  /** Resolve a name/alias to `[key, engine]`, or null. */
@@ -281,6 +316,8 @@ const AI_EXEC = {
281
316
  kimi: (p) => ["-p", p], // kimi prompt mode (prints the response, text by default)
282
317
  qwen: (p) => ["-p", p], // qwen prompt mode (gemini-derived)
283
318
  deepseek: (p) => ["--headless", "-p", p], // deepseek: -p runs one prompt and exits; --headless drops the TUI so stdout is pipe-clean
319
+ // openagents is absent on purpose: it answers no prompts itself, it starts
320
+ // the engines that do. ai() throws a named error rather than picking it.
284
321
  };
285
322
 
286
323
  /** argv that runs `prompt` headlessly on `engine` (throws if it has no headless mode). */
@@ -20,6 +20,8 @@
20
20
  // holds feeds with stable well-known URLs and defers everything else to the
21
21
  // published lists, where the list is somebody else's to maintain.
22
22
 
23
+ import fs from "node:fs";
24
+
23
25
  /**
24
26
  * Bing News query feed — the only search feed left.
25
27
  *
@@ -86,6 +88,53 @@ export function isDeadEndLink(url) {
86
88
  }
87
89
  }
88
90
 
91
+ /** The vendored copy of what profullstack.com/feeds.opml serves. */
92
+ const PROFULLSTACK_OPML = new URL("./profullstack-feeds.opml", import.meta.url);
93
+
94
+ /** slugify() from news.mjs, kept in step by a test rather than imported. */
95
+ function slug(label) {
96
+ return String(label ?? "")
97
+ .toLowerCase()
98
+ .replace(/['’]/g, "")
99
+ .replace(/[^a-z0-9]+/g, "-")
100
+ .replace(/^-+|-+$/g, "")
101
+ .slice(0, 32);
102
+ }
103
+
104
+ /**
105
+ * The profullstack blogs, read from the vendored OPML rather than typed out.
106
+ *
107
+ * Read synchronously and at module load because defaultFeeds() is synchronous —
108
+ * a fresh install must not wait on a network call, or on a promise, to show
109
+ * anything at all. The file ships with the package (`files` includes `src`), so
110
+ * it is there in both the npm and the install.sh channel.
111
+ *
112
+ * Parsed here with a small matcher instead of news.mjs's parseOpml, because
113
+ * news.mjs imports this module and taking the import back the other way makes a
114
+ * cycle. The matcher can afford to be small: this is our own file, flat, and a
115
+ * test asserts the two agree on every feed it contains.
116
+ *
117
+ * A missing or unreadable file degrades to no profullstack defaults rather than
118
+ * throwing, which would take `/news` down entirely over a packaging mistake.
119
+ */
120
+ function profullstackFeeds() {
121
+ let xml;
122
+ try { xml = fs.readFileSync(PROFULLSTACK_OPML, "utf8"); }
123
+ catch { return []; }
124
+
125
+ const feeds = [];
126
+ const seen = new Set();
127
+ for (const tag of xml.match(/<outline\b[^>]*>/gi) ?? []) {
128
+ const attr = (name) => (new RegExp(`\\b${name}="([^"]*)"`, "i").exec(tag) ?? [])[1] ?? "";
129
+ const url = attr("xmlUrl");
130
+ if (!/^https?:\/\//i.test(url) || seen.has(url)) continue;
131
+ seen.add(url);
132
+ const title = attr("title") || attr("text") || url;
133
+ feeds.push({ name: slug(title), title, url, site: attr("htmlUrl"), category: "profullstack" });
134
+ }
135
+ return feeds;
136
+ }
137
+
89
138
  /**
90
139
  * The feeds a fresh install reads.
91
140
  *
@@ -120,25 +169,18 @@ export const DEFAULT_FEEDS = [
120
169
 
121
170
  { name: "npr-politics", title: "NPR — Politics", url: "https://feeds.npr.org/1014/rss.xml", site: "https://www.npr.org", category: "politics" },
122
171
 
123
- // The profullstack blogs, read out of the box. This is the one published list
124
- // small enough to be a default: profullstack.com/feeds.opml is 14 feeds, where
125
- // smallweb is 33,000 and could only ever be searched. Kept in step with that
126
- // file by hand rather than fetched, because defaultFeeds() is synchronous and
127
- // a fresh install must not wait on a network call to show anything at all.
128
- { name: "bittorrented-blog", title: "BitTorrented Blog", url: "https://bittorrented.com/blog/rss.xml", site: "https://bittorrented.com/blog", category: "profullstack" },
129
- { name: "bl0ggers-blog", title: "bl0ggers Blog", url: "https://bl0ggers.com/blog/rss.xml", site: "https://bl0ggers.com/blog", category: "profullstack" },
130
- { name: "c0mpute-blog", title: "c0mpute Blog", url: "https://c0mpute.com/blog/rss.xml", site: "https://c0mpute.com/blog", category: "profullstack" },
131
- { name: "c0upons-blog", title: "c0upons Blog", url: "https://c0upons.com/blog/rss.xml", site: "https://c0upons.com/blog", category: "profullstack" },
132
- { name: "coinpay-blog", title: "CoinPay Blog", url: "https://coinpayportal.com/blog/rss.xml", site: "https://coinpayportal.com/blog", category: "profullstack" },
133
- { name: "crawlproof-blog", title: "CrawlProof Blog", url: "https://crawlproof.com/blog/rss.xml", site: "https://crawlproof.com/blog", category: "profullstack" },
134
- { name: "d0rz-blog", title: "d0rz Blog", url: "https://d0rz.com/blog/rss.xml", site: "https://d0rz.com/blog", category: "profullstack" },
135
- { name: "logicsrc-blog", title: "LogicSRC Blog", url: "https://logicsrc.com/blog/rss.xml", site: "https://logicsrc.com/blog", category: "profullstack" },
136
- { name: "pairux-blog", title: "PairUX Blog", url: "https://pairux.com/blog/rss.xml", site: "https://pairux.com/blog", category: "profullstack" },
137
- { name: "qryptchat-blog", title: "QryptChat Blog", url: "https://qrypt.chat/blog/rss.xml", site: "https://qrypt.chat/blog", category: "profullstack" },
138
- { name: "saasrow-blog", title: "SaaSRow Blog", url: "https://www.saasrow.com/blog/rss.xml", site: "https://www.saasrow.com/blog", category: "profullstack" },
139
- { name: "sh1pt-blog", title: "sh1pt Blog", url: "https://sh1pt.com/blog/rss.xml", site: "https://sh1pt.com/blog", category: "profullstack" },
140
- { name: "threatcrush-blog", title: "ThreatCrush Blog", url: "https://threatcrush.com/blog/rss.xml", site: "https://threatcrush.com/blog", category: "profullstack" },
141
- { name: "ugig-blog", title: "ugig Blog", url: "https://ugig.net/blog/rss.xml", site: "https://ugig.net/blog", category: "profullstack" },
172
+ // The profullstack blogs, read out of the box. Not typed out here: they are
173
+ // parsed from src/profullstack-feeds.opml, which is a copy of what
174
+ // profullstack.com/feeds.opml actually serves. Two hand-maintained lists of
175
+ // the same fourteen blogs is one more than can be kept in step, and the copy
176
+ // that would go stale is this one nobody editing the published OPML has a
177
+ // reason to think about moshcode. Refresh it with:
178
+ //
179
+ // curl -sL https://profullstack.com/feeds.opml -o src/profullstack-feeds.opml
180
+ //
181
+ // test/profullstack-feeds.test.mjs checks that against the live file when
182
+ // MOSHCODE_CHECK_FEED_DRIFT=1 is set.
183
+ ...profullstackFeeds(),
142
184
  ];
143
185
 
144
186
  /**
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <opml version="2.0">
3
+ <head>
4
+ <title>Profullstack Blogs</title>
5
+ <ownerName>Profullstack</ownerName>
6
+ <ownerId>https://profullstack.com</ownerId>
7
+ <dateCreated>Thu, 13 Aug 2026 00:00:00 +0000</dateCreated>
8
+ </head>
9
+ <body>
10
+ <outline text="BitTorrented Blog" title="BitTorrented Blog" type="rss" xmlUrl="https://bittorrented.com/blog/rss.xml" htmlUrl="https://bittorrented.com/blog" />
11
+ <outline text="bl0ggers Blog" title="bl0ggers Blog" type="rss" xmlUrl="https://bl0ggers.com/blog/rss.xml" htmlUrl="https://bl0ggers.com/blog" />
12
+ <outline text="c0mpute Blog" title="c0mpute Blog" type="rss" xmlUrl="https://c0mpute.com/blog/rss.xml" htmlUrl="https://c0mpute.com/blog" />
13
+ <outline text="c0upons Blog" title="c0upons Blog" type="rss" xmlUrl="https://c0upons.com/blog/rss.xml" htmlUrl="https://c0upons.com/blog" />
14
+ <outline text="CoinPay Blog" title="CoinPay Blog" type="rss" xmlUrl="https://coinpayportal.com/blog/rss.xml" htmlUrl="https://coinpayportal.com/blog" />
15
+ <outline text="CrawlProof Blog" title="CrawlProof Blog" type="rss" xmlUrl="https://crawlproof.com/blog/rss.xml" htmlUrl="https://crawlproof.com/blog" />
16
+ <outline text="d0rz Blog" title="d0rz Blog" type="rss" xmlUrl="https://d0rz.com/blog/rss.xml" htmlUrl="https://d0rz.com/blog" />
17
+ <outline text="LogicSRC Blog" title="LogicSRC Blog" type="rss" xmlUrl="https://logicsrc.com/blog/rss.xml" htmlUrl="https://logicsrc.com/blog" />
18
+ <outline text="PairUX Blog" title="PairUX Blog" type="rss" xmlUrl="https://pairux.com/blog/rss.xml" htmlUrl="https://pairux.com/blog" />
19
+ <outline text="QryptChat Blog" title="QryptChat Blog" type="rss" xmlUrl="https://qrypt.chat/blog/rss.xml" htmlUrl="https://qrypt.chat/blog" />
20
+ <outline text="SaaSRow Blog" title="SaaSRow Blog" type="rss" xmlUrl="https://www.saasrow.com/blog/rss.xml" htmlUrl="https://www.saasrow.com/blog" />
21
+ <outline text="sh1pt Blog" title="sh1pt Blog" type="rss" xmlUrl="https://sh1pt.com/blog/rss.xml" htmlUrl="https://sh1pt.com/blog" />
22
+ <outline text="ThreatCrush Blog" title="ThreatCrush Blog" type="rss" xmlUrl="https://threatcrush.com/blog/rss.xml" htmlUrl="https://threatcrush.com/blog" />
23
+ <outline text="ugig Blog" title="ugig Blog" type="rss" xmlUrl="https://ugig.net/blog/rss.xml" htmlUrl="https://ugig.net/blog" />
24
+ </body>
25
+ </opml>
@@ -64,6 +64,16 @@ export const SYNCED_FILES = [
64
64
  // and read by nothing here — moshcode's interest in it begins and ends with
65
65
  // moving it, and a file this does not parse cannot be broken by this.
66
66
  { path: "feeds.opml", json: false, label: "rss feeds" },
67
+ // `/news` and `/rss` keep their subscriptions here — see opmlFile() in
68
+ // news.mjs. A near-identical name sat in this list above it for a while and
69
+ // the two were easy to mistake for each other, so: feeds.opml belongs to
70
+ // tcfeed, news.opml belongs to this. Carrying only the first meant the feeds
71
+ // you actually subscribed to were the one thing `/save` left behind.
72
+ //
73
+ // Deliberately no cap change. A subscription list past MAX_FILE_BYTES is
74
+ // reported as skipped rather than failing the snapshot, which is the right
75
+ // answer for a list that got big by importing somebody else's.
76
+ { path: "news.opml", json: false, label: "news subscriptions" },
67
77
  ];
68
78
 
69
79
  /**