moshcode 0.25.1 → 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.
- package/.claude-plugin/marketplace.json +23 -0
- package/README.md +73 -0
- package/bin/moshcode.mjs +15 -1
- package/package.json +3 -1
- package/plugins/ticker/.claude-plugin/plugin.json +13 -0
- package/plugins/ticker/README.md +49 -0
- package/plugins/ticker/commands/discover.md +37 -0
- package/plugins/ticker/commands/lookup.md +27 -0
- package/plugins/ticker/commands/reports.md +30 -0
- package/plugins/ticker/commands/research.md +29 -0
- package/plugins/ticker/commands/signals.md +30 -0
- package/plugins/ticker/commands/ticker.md +42 -0
- package/prd/0008-ticker-research-and-plugin-marketplace.md +130 -0
- package/prd/README.md +1 -0
- package/src/advisor.mjs +588 -0
- package/src/cli-schema.mjs +97 -0
- package/src/integrations.mjs +76 -2
- package/src/plugins.mjs +121 -0
- package/src/socials.mjs +72 -0
- package/src/tui.mjs +38 -1
package/src/cli-schema.mjs
CHANGED
|
@@ -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/integrations.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/plugins.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/socials.mjs
ADDED
|
@@ -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) {
|