moshcode 0.25.0 → 0.26.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.
@@ -289,6 +289,48 @@ export const CORE_CLI_COMMANDS = [
289
289
  seeAlso: ["tools", "install"],
290
290
  note: "buy/sell inject --dry-run unless --submit is present. Alpaca defaults to paper trading; live trading requires its separate --live opt-in.",
291
291
  },
292
+ {
293
+ name: "ticker",
294
+ group: "tools",
295
+ description: "equity research from advis0r.com",
296
+ synopsis: [
297
+ ["moshcode ticker <symbol>", "the stored research report for one ticker"],
298
+ ["moshcode ticker <verb> [args…]", ""],
299
+ ],
300
+ verbs: "TICKER_VERBS",
301
+ flags: [
302
+ ["--json", "print the raw API response", ""],
303
+ ["--limit <n>", "cap results (search/lookup/reports/discover)", "the API's own default"],
304
+ ["--sort <s>", "reports order: recent | score | ticker", "score"],
305
+ ["--horizon <n>", "discover: quarters to look ahead (1 or 2)", "2"],
306
+ ["--provider <p>", "discover: analysis provider", "offline"],
307
+ ],
308
+ examples: [
309
+ ["moshcode ticker NVDA", "score, technicals, thesis, signals, sources"],
310
+ ["moshcode ticker lookup rivian", "company name → RIVN"],
311
+ ["moshcode ticker signals AAPL", "what was actually said, with sources"],
312
+ ["moshcode ticker search 'data center'", "across every indexed transcript"],
313
+ ["moshcode ticker reports --limit 10", "the stored index, best score first"],
314
+ ],
315
+ seeAlso: ["trade", "plugin", "tools"],
316
+ note: "research aid, not advice — reports are stored snapshots and every one prints when it was generated. Set MOSHCODE_ADVISOR_URL to point at another instance.",
317
+ },
318
+ { name: "advisor", aliasOf: "ticker", description: "alias for ticker" },
319
+ {
320
+ name: "plugin",
321
+ group: "extend",
322
+ description: "install moshcode's slash commands into Claude Code",
323
+ synopsis: [["moshcode plugin <verb> [name]", ""]],
324
+ verbs: "PLUGIN_VERBS",
325
+ flags: [["--json", "machine-readable", ""]],
326
+ examples: [
327
+ ["moshcode plugin install", "add the marketplace and install ticker"],
328
+ ["moshcode plugin list", "what this marketplace ships, and what is installed"],
329
+ ],
330
+ seeAlso: ["skill", "mcp", "ticker"],
331
+ note: "Claude Code is the only engine with a plugin primitive; the others are reported as skipped, exactly as they are for skills.",
332
+ },
333
+ { name: "plugins", aliasOf: "plugin", description: "alias for plugin" },
292
334
  {
293
335
  name: "commands",
294
336
  group: "script",
@@ -466,6 +508,51 @@ export const DNS_VERBS = [
466
508
  { name: "trust", description: "trust one name's certificate, after checking it against the registry pin" },
467
509
  ];
468
510
 
511
+ /**
512
+ * `ticker`'s verbs.
513
+ *
514
+ * `report` exists so a symbol that collides with a verb name still has an
515
+ * unambiguous spelling; without it, the bare-symbol shortcut would have no
516
+ * escape hatch. src/advisor.mjs owns the parser and test/advisor.test.mjs
517
+ * fails when the two lists disagree.
518
+ */
519
+ export const TICKER_VERBS = [
520
+ { name: "report", description: "the stored research report for one ticker", synopsis: [["moshcode ticker report <symbol>", "same as `moshcode ticker <symbol>`"]] },
521
+ { name: "signals", description: "every extracted signal for a ticker", synopsis: [["moshcode ticker signals <symbol>", ""]] },
522
+ {
523
+ name: "search", description: "full-text search across indexed transcripts",
524
+ synopsis: [["moshcode ticker search <words…> [--limit n]", ""]],
525
+ },
526
+ {
527
+ name: "lookup", description: "find a ticker by company name",
528
+ synopsis: [["moshcode ticker lookup <company…> [--limit n]", "rivian → RIVN"]],
529
+ },
530
+ {
531
+ name: "reports", description: "every stored report",
532
+ synopsis: [["moshcode ticker reports [--sort recent|score|ticker] [--limit n]", ""]],
533
+ },
534
+ {
535
+ name: "discover", description: "a ranked watchlist for a topic",
536
+ synopsis: [["moshcode ticker discover [topic…] [--horizon 1|2] [--provider p] [--limit n]", ""]],
537
+ note: "ranks by analyzing each candidate — this one takes minutes, not milliseconds.",
538
+ },
539
+ { name: "tickers", description: "every ticker present in the index", synopsis: [["moshcode ticker tickers", ""]] },
540
+ { name: "stats", description: "index coverage counts", synopsis: [["moshcode ticker stats", ""]] },
541
+ { name: "open", description: "open the shareable report page in a browser", synopsis: [["moshcode ticker open <symbol>", ""]] },
542
+ ];
543
+
544
+ export const PLUGIN_VERBS = [
545
+ {
546
+ name: "install", description: "add the marketplace and install a plugin",
547
+ synopsis: [
548
+ ["moshcode plugin install", "the default plugin (ticker)"],
549
+ ["moshcode plugin install <name>", ""],
550
+ ],
551
+ },
552
+ { name: "list", description: "show what the marketplace ships and what is installed", synopsis: [["moshcode plugin list [--json]", ""]] },
553
+ { name: "remove", description: "uninstall a plugin from Claude Code", synopsis: [["moshcode plugin remove <name>", ""]] },
554
+ ];
555
+
469
556
  /** Sub-verb tables, by the name a command's `verbs` field refers to. */
470
557
  export const VERB_TABLES = {
471
558
  MCP_VERBS,
@@ -473,6 +560,8 @@ export const VERB_TABLES = {
473
560
  UPGRADE_TARGETS,
474
561
  DNS_VERBS,
475
562
  TRADE_VERBS,
563
+ TICKER_VERBS,
564
+ PLUGIN_VERBS,
476
565
  };
477
566
 
478
567
  /**
@@ -499,6 +588,14 @@ export const PIT_COMMANDS = [
499
588
  description: "list workflow tools, or run one" },
500
589
  { name: "trade", args: "<verb> [args…]", cli: "trade",
501
590
  description: "look up markets and preview/place Alpaca orders" },
591
+ { name: "ticker", aliases: ["advisor"], args: "<symbol|verb> [args…]", cli: "ticker",
592
+ description: "equity research from advis0r.com" },
593
+ { name: "plugin", aliases: ["plugins"], args: "<verb> [name]", cli: "plugin",
594
+ description: "install moshcode's slash commands into Claude Code" },
595
+ { name: "socials", aliases: ["social"], pitOnly: true,
596
+ description: "list social networks available for posting" },
597
+ { name: "post", args: '<social> "message"', pitOnly: true,
598
+ description: "open a social composer with a prepared post" },
502
599
  { name: "install", args: "<engine|tool>", cli: "install",
503
600
  description: "install an engine or workflow tool" },
504
601
  { name: "upgrade", aliases: ["update"], args: "[name…]", cli: "upgrade",
package/src/engines.mjs CHANGED
@@ -5,11 +5,11 @@
5
5
  //
6
6
  // `agentsView` (optional) is the exact argv that opens the engine's native
7
7
  // agent list/view — used by `/agents <name>` when the engine actually has one
8
- // (claude, opencode). It's the FULL leading args (subcommand + any flags that
9
- // subcommand accepts), because not every agents-subcommand takes the engine's
10
- // bypass flag (e.g. `opencode agent list` takes none). Engines without an
11
- // `agentsView` fall back to `agentArgs` an autonomous session with native
12
- // approvals bypassed/auto-approved.
8
+ // (currently claude). It's the FULL leading args (subcommand + any flags that
9
+ // subcommand accepts). Engines without an `agentsView` fall back to
10
+ // `agentArgs` an autonomous session with native approvals
11
+ // bypassed/auto-approved. Do not use a machine-readable, one-shot list command
12
+ // as an agents view: `/agents` promises to hand the terminal to a live session.
13
13
  import { spawn } from "node:child_process";
14
14
  import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
15
15
  import { homedir, tmpdir } from "node:os";
@@ -22,17 +22,19 @@ export const ENGINES = {
22
22
  desc: "opencode — the open-source coding agent (SST/anomalyco)",
23
23
  bin: "opencode",
24
24
  agentArgs: ["--auto"],
25
- agentsView: ["agent", "list"], // `opencode agent list` — lists agents; the `agent` subcommand takes no bypass flag
26
25
  install: { cmd: "bash", args: ["-c", "curl -fsSL https://opencode.ai/install | bash"] },
27
26
  upgrade: { cmd: "opencode", args: ["upgrade"] },
27
+ // The installer appends this directory to a shell profile. The moshcode
28
+ // process that ran it cannot see that PATH change, so search it directly.
29
+ binDirs: [path.join(homedir(), ".opencode", "bin")],
28
30
  },
29
31
  privacycode: {
30
32
  desc: "privacycode — privacy-first coding agent (profullstack)",
31
33
  bin: "privacycode",
32
34
  // An opencode derivative, so it speaks the same flags/subcommands.
33
35
  agentArgs: ["--auto"],
34
- agentsView: ["agent", "list"],
35
36
  install: { cmd: "sh", args: ["-c", "curl -fsSL https://getprivacycode.com/install | sh"] },
37
+ binDirs: [path.join(homedir(), ".privacycode", "bin")],
36
38
  // Deliberately no native updater. `privacycode upgrade` is opencode's, and
37
39
  // it works out how to update itself by recognising where it was installed —
38
40
  // it knows opencode's own locations, not this fork's ~/.privacycode/bin. It
@@ -263,9 +265,10 @@ export function pickAiEngine(preferred) {
263
265
 
264
266
  /** Engine entries annotated with install status. */
265
267
  export function engineStatus() {
266
- // Search each engine's own install dir as well as PATH — kimi's installer only
267
- // adds ~/.kimi-code/bin to your shell rc, so PATH alone reports it missing in
268
- // the very session that installed it. (Inert for engines without binDirs.)
268
+ // Search each engine's own install dir as well as PATH — several curl-based
269
+ // installers only add their bin directory to a shell rc, so PATH alone
270
+ // reports them missing in the very session that installed them. (Inert for
271
+ // engines without binDirs.)
269
272
  return Object.entries(ENGINES).map(([key, e]) => ({ key, ...e, installed: isInstalled(e.bin, e.binDirs) }));
270
273
  }
271
274
 
@@ -8,8 +8,12 @@ import {
8
8
  import {
9
9
  SKILL_ENGINES, planSkillInstall, runSkillInstall, skillName,
10
10
  } from "./skills.mjs";
11
+ import {
12
+ MARKETPLACE_NAME, PLUGINS, PLUGIN_ENGINES, marketplaceSource, planPluginCommand,
13
+ pluginId, resolvePlugin, runPluginCommand,
14
+ } from "./plugins.mjs";
11
15
  import { catalogList, resolveCatalog } from "./mcp-catalog.mjs";
12
- import { MCP_VERBS, SKILL_VERBS } from "./cli-schema.mjs";
16
+ import { MCP_VERBS, PLUGIN_VERBS, SKILL_VERBS } from "./cli-schema.mjs";
13
17
  import { acid, ash, bone, ok, err, info } from "./ui.mjs";
14
18
 
15
19
  function splitKV(pair) {
@@ -180,7 +184,7 @@ export function printSkillTargets(json = false) {
180
184
 
181
185
  function summarize(results) {
182
186
  for (const r of results) {
183
- if (r.status === "added" || r.status === "installed") console.log(line(r.key, ok(r.status)));
187
+ if (r.status === "added" || r.status === "installed" || r.status === "removed") console.log(line(r.key, ok(r.status)));
184
188
  else if (r.status === "failed") console.log(line(r.key, err(`failed${r.code != null ? ` (code ${r.code})` : r.signal ? ` (${r.signal})` : ""}`)));
185
189
  else if (r.status === "not-installed") console.log(line(r.key, ash("not installed — /install " + r.key)));
186
190
  else console.log(line(r.key, ash(`skipped — ${r.reason}`)));
@@ -263,3 +267,73 @@ export async function skillCommand(tokens, { run, installedSet } = {}) {
263
267
  summarize(results);
264
268
  return anyFailed(results) ? 1 : 0;
265
269
  }
270
+
271
+ /**
272
+ * `/plugin list` — what this marketplace ships, and which engines can take it.
273
+ *
274
+ * Two tables rather than one: the plugin list is a property of moshcode, the
275
+ * engine support is a property of this machine, and merging them into a single
276
+ * list is how "installed" and "installable" get confused.
277
+ */
278
+ export function printPluginTargets(json = false, { installedSet } = {}) {
279
+ const targets = integrationTargetStatus(PLUGIN_ENGINES, { installedSet }).map((t) => ({
280
+ ...t, supported: PLUGIN_ENGINES.includes(t.name),
281
+ }));
282
+ if (json) {
283
+ console.log(JSON.stringify({
284
+ marketplace: { name: MARKETPLACE_NAME, source: marketplaceSource() },
285
+ plugins: PLUGINS,
286
+ engines: targets,
287
+ }, null, 2));
288
+ return;
289
+ }
290
+ console.log(bone(" plugins") + ash(" — install moshcode's slash commands with ") + acid("/plugin install"));
291
+ for (const plugin of PLUGINS) {
292
+ console.log(` ${acid(pluginId(plugin.name).padEnd(18))}${ash(plugin.description)}`);
293
+ console.log(` ${" ".repeat(18)}${ash(plugin.commands.join(" "))}`);
294
+ }
295
+ console.log("");
296
+ for (const target of targets) {
297
+ const dot = target.supported && target.installed ? DOT.installed : DOT.missing;
298
+ console.log(` ${dot} ${bone(target.name.padEnd(9))} ${ash(target.supported ? "plugins supported" : "no plugin primitive")}`);
299
+ }
300
+ }
301
+
302
+ /** Run `/plugin …`. `tokens` are the words after `plugin`. */
303
+ export async function pluginCommand(tokens, { run, installedSet } = {}) {
304
+ const verb = tokens[0];
305
+ if (!verb || verb === "list") {
306
+ printPluginTargets(tokens.slice(1).includes("--json"), { installedSet });
307
+ return 0;
308
+ }
309
+ if (verb !== "install" && verb !== "remove") {
310
+ console.log(err(`unknown plugin verb "${verb}" — try ${PLUGIN_VERBS.map(({ name }) => name).join(", ")}`));
311
+ return 1;
312
+ }
313
+
314
+ const rest = tokens.slice(1).filter((t) => t !== "--json");
315
+ const stray = rest.find((t) => String(t).startsWith("-"));
316
+ if (stray) { console.log(err(`unknown plugin flag "${stray}"`)); return 1; }
317
+
318
+ const plugin = resolvePlugin(rest[0]);
319
+ if (!plugin) {
320
+ console.log(err(`unknown plugin "${rest[0]}" — this marketplace ships ${PLUGINS.map((p) => p.name).join(", ")}`));
321
+ return 1;
322
+ }
323
+
324
+ const source = marketplaceSource();
325
+ console.log(verb === "install"
326
+ ? info(`installing ${bone(pluginId(plugin.name))} ${ash(`from ${source}`)} across plugin engines…`)
327
+ : info(`removing ${bone(pluginId(plugin.name))} from plugin engines…`));
328
+
329
+ const plan = planPluginCommand({ plugin, source }, { installedSet, verb });
330
+ const results = await runPluginCommand(plan, { verb, ...(run ? { run } : {}) });
331
+ summarize(results);
332
+
333
+ // A newly installed plugin is not live in an already-running engine, and the
334
+ // first thing anyone does is type the slash command and conclude it failed.
335
+ if (!anyFailed(results) && verb === "install" && results.some((r) => r.status === "installed")) {
336
+ console.log(info(`restart the engine, then try ${acid(`${plugin.commands[0]} NVDA`)}`));
337
+ }
338
+ return anyFailed(results) ? 1 : 0;
339
+ }
@@ -0,0 +1,121 @@
1
+ // `moshcode plugin` — install moshcode's own slash commands into an engine.
2
+ //
3
+ // The same shape as src/skills.mjs, for the same reason: one source, a plan of
4
+ // per-engine actions, and a summary that names the engines it *skipped* as well
5
+ // as the ones it touched. An engine silently missing from the summary reads as
6
+ // "installed everywhere", which is exactly the confusion prd/0003 R8 exists to
7
+ // prevent.
8
+ //
9
+ // Claude Code is currently the only engine with a plugin primitive. That is a
10
+ // fact about the engines, not an assumption baked into the fan-out — adding a
11
+ // second one means adding a case to pluginInstallActions, nothing else.
12
+ import { ENGINES, isInstalled, ranOk, runCmd } from "./engines.mjs";
13
+
14
+ /** Engines with a plugin primitive. */
15
+ export const PLUGIN_ENGINES = ["claude"];
16
+
17
+ /** The marketplace this repo publishes (see .claude-plugin/marketplace.json). */
18
+ export const MARKETPLACE_NAME = "moshcode";
19
+
20
+ /**
21
+ * Where the marketplace is fetched from. A GitHub `owner/repo` by default;
22
+ * point it at a checkout to test an unreleased plugin:
23
+ * MOSHCODE_PLUGIN_SOURCE=. moshcode plugin install
24
+ */
25
+ export function marketplaceSource(env = process.env) {
26
+ return String(env.MOSHCODE_PLUGIN_SOURCE || "moshcoder/moshcode").trim() || "moshcoder/moshcode";
27
+ }
28
+
29
+ /** The plugins this marketplace ships. Mirrors .claude-plugin/marketplace.json. */
30
+ export const PLUGINS = [
31
+ {
32
+ name: "ticker",
33
+ description: "equity research slash commands backed by advis0r.com",
34
+ commands: ["/ticker", "/signals", "/research", "/lookup", "/reports", "/discover"],
35
+ },
36
+ ];
37
+
38
+ export const DEFAULT_PLUGIN = PLUGINS[0].name;
39
+
40
+ export function resolvePlugin(name) {
41
+ if (!name) return PLUGINS.find((p) => p.name === DEFAULT_PLUGIN) ?? null;
42
+ const key = String(name).toLowerCase().replace(/@.*$/, "");
43
+ return PLUGINS.find((p) => p.name === key) ?? null;
44
+ }
45
+
46
+ /** Fully-qualified plugin id, the form `claude plugin install` disambiguates with. */
47
+ export function pluginId(name) {
48
+ return `${name}@${MARKETPLACE_NAME}`;
49
+ }
50
+
51
+ /**
52
+ * The commands one engine needs to install a plugin.
53
+ *
54
+ * Adding the marketplace is idempotent and separate from installing, so it runs
55
+ * every time: a machine that added the marketplace before this plugin existed
56
+ * would otherwise fail the install with "not found in any marketplace".
57
+ */
58
+ export function pluginInstallActions(key, { plugin, source, scope }) {
59
+ switch (key) {
60
+ case "claude":
61
+ return [
62
+ { cmd: "claude", args: ["plugin", "marketplace", "add", source, ...(scope ? ["--scope", scope] : [])] },
63
+ { cmd: "claude", args: ["plugin", "install", pluginId(plugin.name), ...(scope ? ["--scope", scope] : [])] },
64
+ ];
65
+ default:
66
+ return { skip: "no plugin primitive" };
67
+ }
68
+ }
69
+
70
+ export function pluginRemoveActions(key, { plugin }) {
71
+ switch (key) {
72
+ case "claude":
73
+ return [{ cmd: "claude", args: ["plugin", "uninstall", pluginId(plugin.name)] }];
74
+ default:
75
+ return { skip: "no plugin primitive" };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Plan the fan-out: one entry per engine, with its actions or its skip reason.
81
+ * Derived from ENGINES so an engine added later cannot fall out of the summary.
82
+ */
83
+ export function planPluginCommand(spec, { installedSet, verb = "install" } = {}) {
84
+ const build = verb === "remove" ? pluginRemoveActions : pluginInstallActions;
85
+ const rest = Object.keys(ENGINES).filter((key) => !PLUGIN_ENGINES.includes(key));
86
+ return [...PLUGIN_ENGINES, ...rest].map((key) => {
87
+ const bin = ENGINES[key].bin;
88
+ const installed = installedSet ? installedSet.has(key) : isInstalled(bin, ENGINES[key].binDirs);
89
+ const actions = build(key, spec);
90
+ return Array.isArray(actions)
91
+ ? { key, bin, installed, actions }
92
+ : { key, bin, installed, ...actions };
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Execute a plan. Returns [{ key, status, reason?, code? }] with
98
+ * status one of installed | removed | skipped | failed | not-installed.
99
+ * `run` is injectable for tests.
100
+ */
101
+ export async function runPluginCommand(plan, { run = runCmd, verb = "install" } = {}) {
102
+ const done = verb === "remove" ? "removed" : "installed";
103
+ const results = [];
104
+ for (const item of plan) {
105
+ if (item.skip) { results.push({ key: item.key, status: "skipped", reason: item.skip }); continue; }
106
+ if (!item.installed) { results.push({ key: item.key, status: "not-installed" }); continue; }
107
+ let last = null;
108
+ let failed = false;
109
+ for (const action of item.actions) {
110
+ last = await run(action.cmd, action.args);
111
+ if (!ranOk(last)) { failed = true; break; }
112
+ }
113
+ results.push({
114
+ key: item.key,
115
+ status: failed ? "failed" : done,
116
+ code: last?.code ?? null,
117
+ signal: last?.signal ?? null,
118
+ });
119
+ }
120
+ return results;
121
+ }
@@ -0,0 +1,72 @@
1
+ import { canOpenBrowser, openBrowser } from "./open-url.mjs";
2
+
3
+ const DEFAULT_APP = "https://app.moshcode.sh";
4
+
5
+ export const SOCIALS = [
6
+ {
7
+ name: "bluesky",
8
+ aliases: ["bsky"],
9
+ description: "official Bluesky browser composer",
10
+ },
11
+ {
12
+ name: "nostr",
13
+ aliases: [],
14
+ description: "NIP-07/NIP-46 browser signer + relay publish",
15
+ },
16
+ ];
17
+
18
+ export function resolveSocial(name) {
19
+ const wanted = String(name ?? "").trim().toLowerCase();
20
+ return SOCIALS.find((social) =>
21
+ social.name === wanted || social.aliases.includes(wanted)) ?? null;
22
+ }
23
+
24
+ function appOrigin(env = process.env) {
25
+ return String(env.MOSHCODE_API || DEFAULT_APP).replace(/\/+$/, "");
26
+ }
27
+
28
+ /**
29
+ * Build the browser hand-off without opening anything. Nostr keeps the draft
30
+ * in the fragment so it never reaches app.moshcode.sh access logs or Referer
31
+ * headers; the composer reads it entirely in the browser.
32
+ */
33
+ export function socialPostUrl(name, message, { env = process.env } = {}) {
34
+ const social = resolveSocial(name);
35
+ if (!social) return null;
36
+ const text = String(message ?? "");
37
+ if (social.name === "bluesky") {
38
+ return `https://bsky.app/intent/compose?${new URLSearchParams({ text })}`;
39
+ }
40
+ return `${appOrigin(env)}/socials/nostr#${new URLSearchParams({ text })}`;
41
+ }
42
+
43
+ export function socialRoster() {
44
+ return SOCIALS.map((social) => ({ ...social, aliases: [...social.aliases] }));
45
+ }
46
+
47
+ /**
48
+ * Open a provider composer. Posting remains an explicit browser confirmation:
49
+ * Bluesky requires it, and Nostr asks the browser signer before relay publish.
50
+ */
51
+ export function postSocial(args, {
52
+ env = process.env,
53
+ canOpen = canOpenBrowser,
54
+ open = openBrowser,
55
+ } = {}) {
56
+ const [requested, ...words] = Array.isArray(args) ? args : [];
57
+ const social = resolveSocial(requested);
58
+ if (!requested) return { ok: false, error: 'usage: /post <social> "message"' };
59
+ if (!social) {
60
+ return {
61
+ ok: false,
62
+ error: `unknown social "${requested}". try: ${SOCIALS.map((entry) => entry.name).join(", ")}`,
63
+ };
64
+ }
65
+
66
+ const message = words.join(" ").trim();
67
+ if (!message) return { ok: false, error: 'usage: /post <social> "message"' };
68
+
69
+ const url = socialPostUrl(social.name, message, { env });
70
+ const opened = Boolean(canOpen() && open(url));
71
+ return { ok: true, social: social.name, message, url, opened };
72
+ }
package/src/tui.mjs CHANGED
@@ -10,6 +10,7 @@ import path from "node:path";
10
10
  import { ENGINES, agentLaunchArgs, resolveEngine, engineStatus, openSession } from "./engines.mjs";
11
11
  import { TOOLS, resolveTool, toolStatus, openTool } from "./tools.mjs";
12
12
  import { tradeArgs, tradeUsage } from "./trade.mjs";
13
+ import { postSocial, socialRoster } from "./socials.mjs";
13
14
  import { runUpgrade } from "./upgrade.mjs";
14
15
  import { locate, tilde } from "./pwd.mjs";
15
16
  import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
@@ -18,7 +19,9 @@ import { createMirror, teeOutput } from "./mirror.mjs";
18
19
  import { fetchMotdAd } from "./ads.mjs";
19
20
  import { runScript } from "./runtime.mjs";
20
21
  import { moshVocabulary } from "./commands.mjs";
21
- import { mcpCommand, skillCommand } from "./integrations.mjs";
22
+ import { mcpCommand, pluginCommand, skillCommand } from "./integrations.mjs";
23
+ import { tickerCommand } from "./advisor.mjs";
24
+ import { canOpenBrowser, openBrowser } from "./open-url.mjs";
22
25
  import { banner, hr, acid, ash, bone, dim, ok, err, warn, info, moshcodeVersion } from "./ui.mjs";
23
26
  import { CORE_CLI_COMMAND_NAMES } from "./cli-schema.mjs";
24
27
  import { findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
@@ -156,6 +159,15 @@ function printTools() {
156
159
  console.log(ash(" → ") + acid("https://dev.profullstack.com/"));
157
160
  }
158
161
 
162
+ function printSocials() {
163
+ console.log(bone(" socials") + ash(" — compose with ") + acid('/post <social> "message"'));
164
+ for (const social of socialRoster()) {
165
+ const aliases = social.aliases.length ? ` (${social.aliases.join(", ")})` : "";
166
+ console.log(` ${acid("●")} ${bone(social.name.padEnd(9))} ${ash(social.description + aliases)}`);
167
+ }
168
+ console.log(ash(" the browser always asks you to confirm before anything is published"));
169
+ }
170
+
159
171
  /**
160
172
  * The moshscript vocabulary, split the way the CLI's help splits it.
161
173
  *
@@ -660,6 +672,31 @@ export async function tui() {
660
672
  rl = mkrl();
661
673
  continue;
662
674
  }
675
+ // `/ticker` renders in the pit rather than handing the terminal to a tool:
676
+ // there is no advis0r binary to launch, only a public read-only API.
677
+ if (cmd === "ticker" || cmd === "advisor") {
678
+ await tickerCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
679
+ continue;
680
+ }
681
+ if (cmd === "plugin" || cmd === "plugins") {
682
+ await pluginCommand(rest);
683
+ continue;
684
+ }
685
+ if (cmd === "socials" || cmd === "social") {
686
+ printSocials();
687
+ continue;
688
+ }
689
+ if (cmd === "post") {
690
+ const result = postSocial(rest);
691
+ if (!result.ok) { console.log(err(result.error)); continue; }
692
+ if (result.opened) {
693
+ console.log(ok(`opened the ${result.social} composer — confirm the post in your browser 🤘`));
694
+ } else {
695
+ console.log(info(`open this ${result.social} composer in a browser:`));
696
+ console.log(` ${result.url}`);
697
+ }
698
+ continue;
699
+ }
663
700
  // Bare engine name → open it.
664
701
  const resolved = resolveEngine(cmd);
665
702
  if (resolved) {